From a178d245c5661871cc5673d899852cfa0df91f03 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 16:45:44 +0900 Subject: [PATCH 1/5] feat(ask): cite persisted image evidence for cited posts An Ask answer citing a post whose evidence actually came from an embedded picture (a screenshot, a diagram) read as an unmarked text claim -- no way to tell the citation was image-sourced. Raw image bytes are never sent to the client anywhere in this codebase (only persisted OCR/caption/tags -- see lineageweave/image_content.py); this reuses that same never-raw-bytes description GET /api/posts/{id}/content already renders, scoped to already-cited posts. Backend: cited_post_images (backend/app/post_chat_ingestion.py) reads post_content_image/post_content_image_tag for the cited post ids, no extra ABAC check needed (cited_post_ids only ever come from gather_global_chat_sources's already-authorized source set, same trust boundary cited_post_evidence/cited_post_summaries rely on). Wired into POST /api/ask as a new cited_post_images response field. Frontend: AskAgentPanel renders an "Image evidence" line under a cited post when present, with the persisted caption and OCR text. Adds the ko / zh / ja / vi translations for the two new strings. Part of the Ask Agent temporal/lineage/evidence goal (checkpoint 3 of 4). --- backend/app/main.py | 5 +++ backend/app/post_chat_ingestion.py | 56 ++++++++++++++++++++++++++++++ frontend/src/App.test.tsx | 38 ++++++++++++++++++++ frontend/src/App.tsx | 11 ++++++ frontend/src/api.ts | 11 ++++++ frontend/src/i18n.ts | 8 +++++ tests/test_post_chat_ingestion.py | 56 ++++++++++++++++++++++++++++++ 7 files changed, 185 insertions(+) diff --git a/backend/app/main.py b/backend/app/main.py index fb943315f..d80c811fd 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -167,6 +167,7 @@ ) from backend.app.lineage_ingestion import rebuild_lineage, visible_lineage_graph from backend.app.post_chat_ingestion import ( + cited_post_images, fetch_persisted_chat, fetch_persisted_chats, find_linked_post_ids, @@ -2643,6 +2644,7 @@ async def ask_agent( "cited_posts": [], "source_post_ids": [], "cited_post_evidence": [], + "cited_post_images": [], "next_action": "No authorized source posts are available for this question.", } try: @@ -2653,11 +2655,14 @@ async def ask_agent( f"Ask Agent is unavailable: {exc}", ) from exc cited_ids = list(answer.cited_post_ids) + async with pool.acquire() as conn: + images = await cited_post_images(conn, cited_ids) return { "answer_text": answer.answer_text, "cited_post_ids": cited_ids, "cited_posts": cited_post_summaries(sources, cited_ids), "cited_post_evidence": cited_post_evidence(sources, cited_ids), + "cited_post_images": images, "source_post_ids": [source.post_id for source in sources], } diff --git a/backend/app/post_chat_ingestion.py b/backend/app/post_chat_ingestion.py index 71c0f2053..397ccd76b 100644 --- a/backend/app/post_chat_ingestion.py +++ b/backend/app/post_chat_ingestion.py @@ -553,6 +553,62 @@ async def gather_global_chat_sources( return sources +async def cited_post_images( + conn: asyncpg.Connection, + cited_post_ids: list[str], +) -> list[dict[str, Any]]: + """Persisted image evidence (caption/OCR/tags) for already-cited posts. + + Global Ask cites a post's *text*; when that post's evidence actually + came from an embedded picture (a screenshot, a diagram), the reader has + no way to tell the difference -- the answer just reads as a text claim. + This surfaces the same persisted, never-raw-bytes image description + `GET /api/posts/{id}/content` already renders (`post_content_image`, + ADR-tracked alongside its region locations), scoped to the posts this + answer already cited. + + No ABAC re-check here: `cited_post_ids` only ever contains ids drawn + from `gather_global_chat_sources`'s already-authorized source set, the + same trust boundary `cited_post_evidence`/`cited_post_summaries` rely + on (`lineageweave.post_chat`). + """ + if not cited_post_ids: + return [] + rows = await conn.fetch( + """ + select unit.post_id, unit.unit_index, image.mime_type, + image.description_status_code, image.extracted_text, image.caption, + coalesce( + array_agg(tag.tag_text order by tag.tag_text) + filter (where tag.tag_text is not null), + '{}'::text[] + ) as tags + from post_content_unit unit + join post_content_image image + on image.post_content_unit_id = unit.post_content_unit_id + left join post_content_image_tag tag + on tag.post_content_image_id = image.post_content_image_id + where unit.post_id = any($1::uuid[]) + group by unit.post_id, unit.unit_index, image.mime_type, + image.description_status_code, image.extracted_text, image.caption + order by unit.post_id, unit.unit_index + """, + cited_post_ids, + ) + return [ + { + "post_id": str(row["post_id"]), + "unit_index": row["unit_index"], + "mime_type": row["mime_type"], + "status_code": row["description_status_code"], + "extracted_text": row["extracted_text"], + "caption": row["caption"], + "tags": list(row["tags"] or []), + } + for row in rows + ] + + @dataclass(frozen=True) class SeededChat: """Synthetic Q&A for a reconstruct/calendar/demo fixture -- not an LLM.""" diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 7462abd2c..de8d2821f 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -90,6 +90,7 @@ describe("App, authenticated", () => { customerEntityHierarchy?: boolean; staleSummary?: boolean; contentAfterSummary?: boolean; + askImageCitation?: boolean; }): ReturnType & { releaseMe: () => void } { const statusLabel: Record = { open: "Open", @@ -1560,6 +1561,19 @@ describe("App, authenticated", () => { ], }, ], + cited_post_images: options?.askImageCitation + ? [ + { + post_id: "post-2", + unit_index: 1, + mime_type: "image/png", + status_code: "described", + extracted_text: "Error code 500 on checkout", + caption: "Screenshot of the checkout error", + tags: ["screenshot", "error"], + }, + ] + : [], source_post_ids: ["post-1", "post-2"], }), ); @@ -1674,6 +1688,30 @@ describe("App, authenticated", () => { expect(screen.queryByText(/ontology_iri|contextual_orchestrator/i)).not.toBeInTheDocument(); }); + it("cites a cited post's persisted image evidence under that post", async () => { + stubBackend({ askImageCitation: true }); + render(); + expect(await screen.findByRole("button", { name: "View post: Public post" })).toBeInTheDocument(); + await userEvent.click(screen.getByRole("button", { name: "Ask Agent" })); + await userEvent.type(screen.getByRole("textbox", { name: "Ask a question" }), "Which project?"); + await userEvent.click(screen.getByRole("button", { name: "Ask" })); + + expect(await screen.findByText(/Image evidence: Screenshot of the checkout error/)).toBeInTheDocument(); + expect(screen.getByText(/Error code 500 on checkout/)).toBeInTheDocument(); + }); + + it("shows no image evidence line when the answer cites no image", async () => { + stubBackend(); + render(); + expect(await screen.findByRole("button", { name: "View post: Public post" })).toBeInTheDocument(); + await userEvent.click(screen.getByRole("button", { name: "Ask Agent" })); + await userEvent.type(screen.getByRole("textbox", { name: "Ask a question" }), "Which project?"); + await userEvent.click(screen.getByRole("button", { name: "Ask" })); + + expect(await screen.findByRole("list", { name: "Evidence facts" })).toBeInTheDocument(); + expect(screen.queryByText(/Image evidence:/)).not.toBeInTheDocument(); + }); + it("labels the Customer Master entity level and Keymen side, never the raw lookup code", async () => { // Live UI finding (2026-08-19): read_customer_master() skipped the // common_lookup_value join both endpoints elsewhere already use, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6fba0dd41..e5c6d4bee 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4537,6 +4537,17 @@ function AskAgentPanel({ ))} ) : null} + {answer.cited_post_images + ?.filter((image) => image.post_id === post.post_id) + .map((image) => ( +

+ {t("Image evidence")}: {image.caption ?? t("Untitled image")} + {image.extracted_text ? ` — ${image.extracted_text}` : ""} +

+ ))} ))} diff --git a/frontend/src/api.ts b/frontend/src/api.ts index cd0141a32..e68eb358a 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -301,11 +301,22 @@ export interface ChatHistory { exchanges: ChatExchange[]; } +export interface CitedPostImage { + post_id: string; + unit_index: number; + mime_type: string; + status_code: string; + extracted_text: string | null; + caption: string | null; + tags: string[]; +} + export interface AskAgentResponse { answer_text: string; cited_post_ids: string[]; cited_posts?: CitedPostRef[]; cited_post_evidence?: CitedPostEvidence[]; + cited_post_images?: CitedPostImage[]; source_post_ids: string[]; next_action?: string; } diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 650acfca8..45a4e560b 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -167,6 +167,8 @@ const TRANSLATIONS: Partial>> = { "Search related posts": "관련 글 검색", "Search related posts for: {name}": "{name} 관련 글 검색", "Evidence facts": "근거 사실", + "Image evidence": "이미지 근거", + "Untitled image": "제목 없는 이미지", "Source field hint": "원천 필드 힌트", "Semantic project": "의미 기반 프로젝트", "Semantic role": "의미 기반 역할", @@ -506,6 +508,8 @@ const TRANSLATIONS: Partial>> = { "Search related posts": "搜索相关文章", "Search related posts for: {name}": "搜索与{name}相关的文章", "Evidence facts": "证据事实", + "Image evidence": "图像证据", + "Untitled image": "无标题图像", "Source field hint": "来源字段提示", "Semantic project": "语义项目", "Semantic role": "语义角色", @@ -868,6 +872,8 @@ const TRANSLATIONS: Partial>> = { "Search related posts": "関連投稿を検索", "Search related posts for: {name}": "{name}の関連投稿を検索", "Evidence facts": "証拠の事実", + "Image evidence": "画像証拠", + "Untitled image": "無題の画像", "Source field hint": "原典フィールドのヒント", "Semantic project": "意味的なプロジェクト", "Semantic role": "意味的な役割", @@ -1206,6 +1212,8 @@ const TRANSLATIONS: Partial>> = { "Search related posts": "Tìm bài viết liên quan", "Search related posts for: {name}": "Tìm bài viết liên quan đến {name}", "Evidence facts": "Sự kiện bằng chứng", + "Image evidence": "Bằng chứng hình ảnh", + "Untitled image": "Hình ảnh chưa đặt tên", "Source field hint": "Gợi ý trường nguồn", "Semantic project": "Dự án ngữ nghĩa", "Semantic role": "Vai trò ngữ nghĩa", diff --git a/tests/test_post_chat_ingestion.py b/tests/test_post_chat_ingestion.py index 92351ebc1..25c1d1a52 100644 --- a/tests/test_post_chat_ingestion.py +++ b/tests/test_post_chat_ingestion.py @@ -8,6 +8,7 @@ from backend.app.post_chat_ingestion import ( LinkedPostIds, + cited_post_images, fetch_persisted_chat, fetch_persisted_chats, gather_chat_sources, @@ -293,3 +294,58 @@ def test_contextual_chat_client_rejects_malformed_provider_response(monkeypatch: client = ContextualOrchestratorPostChatClient("https://orchestrator", "secret") with pytest.raises(ValueError, match="required format"): client.answer("Question", [ChatSourceDocument("post-a", "Evidence A", "body")]) + + +class _ImageFakeConnection: + def __init__(self, rows: list[dict[str, object]]) -> None: + self.rows = rows + self.queries: list[tuple[str, tuple[object, ...]]] = [] + + async def fetch(self, query: str, *args: object) -> list[dict[str, object]]: + self.queries.append((query, args)) + cited_ids = set(args[0]) if args else set() + return [row for row in self.rows if row["post_id"] in cited_ids] + + +def test_cited_post_images_returns_persisted_captions_for_cited_posts_only() -> None: + connection = _ImageFakeConnection( + [ + { + "post_id": "post-a", + "unit_index": 2, + "mime_type": "image/png", + "description_status_code": "described", + "extracted_text": "Error code 500 on checkout", + "caption": "Screenshot of the checkout error", + "tags": ["screenshot", "error"], + }, + { + "post_id": "post-not-cited", + "unit_index": 0, + "mime_type": "image/png", + "description_status_code": "described", + "extracted_text": "irrelevant", + "caption": "irrelevant", + "tags": [], + }, + ] + ) + images = asyncio.run(cited_post_images(connection, ["post-a"])) + assert images == [ + { + "post_id": "post-a", + "unit_index": 2, + "mime_type": "image/png", + "status_code": "described", + "extracted_text": "Error code 500 on checkout", + "caption": "Screenshot of the checkout error", + "tags": ["screenshot", "error"], + } + ] + + +def test_cited_post_images_with_no_citations_skips_the_query() -> None: + connection = _ImageFakeConnection([]) + images = asyncio.run(cited_post_images(connection, [])) + assert images == [] + assert connection.queries == [] From c7f4d6c1133e680befe076442f16ba5f1c722405 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 19:10:19 -0700 Subject: [PATCH 2/5] feat(ask): show cited-post evidence in a Layer Popup (#420) * feat(ask): show cited-post evidence in a Layer Popup Reading an Ask answer's evidence today means either scanning the inline fact list or leaving the answer entirely to open the full post popup. Add AskEvidenceLayerPopup (frontend/src/components/), a focused modal layer -- opened via a new "View evidence" button per citation -- showing that post's text evidence facts (checkpoint 3's cited_post_evidence) and image evidence (checkpoint 3's cited_post_images) without navigating away from the answer. Proper dialog semantics: role="dialog", aria-modal, Escape-to-close, backdrop-click-to-close, initial focus on the panel -- stricter accessibility than the existing PostDetailPopup, which has none of these. Extracted chatEvidenceKindLabel into evidenceKindLabels.ts so both App.tsx and the new component share one label map instead of drifting. Stacked on #419 (checkpoint 3): the popup's image-evidence section needs that PR's cited_post_images field to be meaningful. Part of the Ask Agent temporal/lineage/evidence goal (checkpoint 4 of 4). * test(ask): cover evidence dialog edge cases * fix(ask): contain evidence dialog focus * docs(storybook): cover blank evidence caption * docs(ask): trace modal accessibility standard * docs(storybook): inventory Ask evidence layer * docs(changelog): record Ask evidence layer * fix(changelog): restore historical entries * test(ask): cover modal exit focus and source transition * fix(ask): restore focus when evidence modal exits * fix(frontend): drop stray pre-login AdminPanel, restore return-URL persistence The pre-login screen rendered AdminPanel with a possibly-undefined accessToken (a TS6192/TS2322 build break) and never used the persisted return-URL helpers on the login redirect. Same fix as LineageWeave#456 on main, applied here since this stack predates that fix. --- docs/doctoring/DESIGN_TOKEN_REFERENCES.md | 9 +- docs/storybook-inventory.md | 1 + frontend/src/App.test.tsx | 21 +++ frontend/src/App.tsx | 41 +++-- .../AskEvidenceLayerPopup.stories.tsx | 94 ++++++++++ .../components/AskEvidenceLayerPopup.test.tsx | 136 +++++++++++++++ .../src/components/AskEvidenceLayerPopup.tsx | 163 ++++++++++++++++++ frontend/src/evidenceKindLabels.ts | 12 ++ frontend/src/i18n.ts | 8 + 9 files changed, 471 insertions(+), 14 deletions(-) create mode 100644 frontend/src/components/AskEvidenceLayerPopup.stories.tsx create mode 100644 frontend/src/components/AskEvidenceLayerPopup.test.tsx create mode 100644 frontend/src/components/AskEvidenceLayerPopup.tsx create mode 100644 frontend/src/evidenceKindLabels.ts diff --git a/docs/doctoring/DESIGN_TOKEN_REFERENCES.md b/docs/doctoring/DESIGN_TOKEN_REFERENCES.md index 717954045..c22317628 100644 --- a/docs/doctoring/DESIGN_TOKEN_REFERENCES.md +++ b/docs/doctoring/DESIGN_TOKEN_REFERENCES.md @@ -1,8 +1,8 @@ # Design-token and Storybook traceability **Status:** Active PR evidence; not protected-main truth until merge. -**Scope:** `frontend/src/styles/tokens.css`, repeated chip/close modules, and -the Storybook inventory. +**Scope:** `frontend/src/styles/tokens.css`, repeated chip/close modules, the +Ask evidence dialog, and the Storybook inventory. ## Standards mapped to implementation @@ -11,6 +11,7 @@ the Storybook inventory. | W3C Design Tokens Format Module 2025.10 | Name color, space, type, and radius once; consume those names from repeated objects. | `frontend/src/styles/tokens.css` defines `--color-*`, `--space-*`, `--size-control-min`, `--radius-chip`, `--radius-control`, `--radius-panel`, and `--font-*`. `CitationChip`, `PopupCloseButton`, `CutoffKnownBody`, and `LineageEntityPicker` read those names through `App.css`. | | Storybook for React & Vite | Catalog repeated controls so a buyer can try the next click without reading `App.tsx`. | `frontend/src/components/*.stories.tsx` and `docs/storybook-inventory.md`. | | WCAG 2.2 | Give interactive controls programmatic names and announce an asynchronous evidence failure instead of leaving a perpetual loading state. | Component interaction tests exercise the named controls; `EvidencePanel` exposes its terminal failure with `role="alert"`. This is targeted evidence, not a claim of complete WCAG conformance. | +| WAI-ARIA APG Dialog (Modal) Pattern | A surface marked `aria-modal="true"` must behave modally: focus moves inside, `Tab` and `Shift+Tab` remain inside, and `Escape` closes the layer. | `AskEvidenceLayerPopup` moves initial focus inside the dialog and explicitly cycles forward/backward keyboard focus between its actionable controls; component tests cover both focus-loop directions and Escape. Its evidence lists use dialog-specific accessible labels so assistive technology can distinguish the modal list from the still-rendered inline answer. | ## APA 7th references @@ -23,3 +24,7 @@ https://storybook.js.org/docs/get-started/frameworks/react-vite World Wide Web Consortium. (2024). *Web Content Accessibility Guidelines (WCAG) 2.2* (W3C Recommendation). https://www.w3.org/TR/WCAG22/ + +World Wide Web Consortium. (n.d.). *Dialog (modal) pattern*. WAI-ARIA +Authoring Practices Guide. Retrieved August 22, 2026, from +https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/ diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index 28c59bd48..a92dccf02 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -6,6 +6,7 @@ buyer-facing control you can click before changing product CSS. | Story | Buyer next action | Token / module | |---|---|---| | `Evidence/CitationChip` | Click a cited title to open that source post. | `--color-chip-border`, `--radius-chip`, `CitationChip` | +| `Evidence/AskEvidenceLayerPopup` | Inspect one citation without leaving the answer; close to continue the answer or open the complete source post. Stories cover text/image evidence, no-evidence, missing OCR, null caption, and blank-caption fallback states. | shared popup tokens through `App.css`, `PopupCloseButton`, `AskEvidenceLayerPopup` | | `AnalysisRun/CutoffKnownBody` | Read the cutoff-known sentence, then compare it with the live body below. | `--color-accent-border`, `--space-panel-block`, `--radius-panel`, `CutoffKnownBody` | | `Analysis/LineageEntityPicker` | Choose which corp to reconstruct, then click Request a lineage reconstruction. | `--space-control-gap`, `--size-control-min`, `--radius-control`, `LineageEntityPicker` | | `Chrome/PopupCloseButton` | Close the evidence panel or post popup. | `--space-close-inset`, `--font-size-close`, `PopupCloseButton` | diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index de8d2821f..8bbf6acce 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -1712,6 +1712,27 @@ describe("App, authenticated", () => { expect(screen.queryByText(/Image evidence:/)).not.toBeInTheDocument(); }); + it("opens a cited post's evidence in a Layer Popup without leaving the answer", async () => { + stubBackend({ askImageCitation: true }); + render(); + expect(await screen.findByRole("button", { name: "View post: Public post" })).toBeInTheDocument(); + await userEvent.click(screen.getByRole("button", { name: "Ask Agent" })); + await userEvent.type(screen.getByRole("textbox", { name: "Ask a question" }), "Which project?"); + await userEvent.click(screen.getByRole("button", { name: "Ask" })); + + await userEvent.click(await screen.findByRole("button", { name: "View evidence" })); + + const dialog = await screen.findByRole("dialog", { name: "Linked post" }); + expect(dialog).toBeInTheDocument(); + expect(within(dialog).getByText(/project: Semantic project/)).toBeInTheDocument(); + expect(within(dialog).getByText("Screenshot of the checkout error")).toBeInTheDocument(); + + await userEvent.click(screen.getByRole("button", { name: "Close evidence panel" })); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + // The answer itself is still on screen -- the layer never navigated away. + expect(screen.getByRole("button", { name: "View evidence" })).toBeInTheDocument(); + }); + it("labels the Customer Master entity level and Keymen side, never the raw lookup code", async () => { // Live UI finding (2026-08-19): read_customer_master() skipped the // common_lookup_value join both endpoints elsewhere already use, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e5c6d4bee..8fff1651b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -85,7 +85,9 @@ import { import { CitationChip } from "./components/CitationChip"; import { CutoffKnownBody } from "./components/CutoffKnownBody"; import { LineageEntityPicker } from "./components/LineageEntityPicker"; +import { AskEvidenceLayerPopup } from "./components/AskEvidenceLayerPopup"; import { PopupCloseButton } from "./components/PopupCloseButton"; +import { chatEvidenceKindLabel } from "./evidenceKindLabels"; import { BuyerNav, type BuyerDestination } from "./components/BuyerNav"; import { LineageDag } from "./LineageDag"; import { PostBody } from "./PostBody"; @@ -777,16 +779,6 @@ function projectProvenanceLabel(provenance: string): string { return t(PROJECT_PROVENANCE_LABELS[provenance] ?? "Recorded evidence"); } -const CHAT_EVIDENCE_KIND_LABELS: Record = { - source_field: "Source field hint", - semantic_project: "Semantic project", - semantic_role: "Semantic role", - semantic_keyman: "Semantic Keyman", -}; - -function chatEvidenceKindLabel(kind: string): string { - return t(CHAT_EVIDENCE_KIND_LABELS[kind] ?? "Evidence"); -} const VERIFICATION_BADGE: Record = { verify_pending: "Not yet checked", @@ -4477,6 +4469,7 @@ function AskAgentPanel({ const [answer, setAnswer] = useState(null); const [error, setError] = useState(null); const [asking, setAsking] = useState(false); + const [evidenceLayerPostId, setEvidenceLayerPostId] = useState(null); async function handleAsk() { const normalized = question.trim(); @@ -4525,6 +4518,13 @@ function AskAgentPanel({ + {answer.cited_post_evidence?.find((item) => item.post_id === post.post_id)?.facts.length ? (
    {answer.cited_post_evidence @@ -4555,6 +4555,23 @@ function AskAgentPanel({ )} )} + {evidenceLayerPostId && answer ? ( + post.post_id === evidenceLayerPostId)?.post_title ?? + evidenceLayerPostId + } + facts={ + answer.cited_post_evidence?.find((item) => item.post_id === evidenceLayerPostId)?.facts ?? [] + } + images={ + answer.cited_post_images?.filter((image) => image.post_id === evidenceLayerPostId) ?? [] + } + onClose={() => setEvidenceLayerPostId(null)} + onOpenPost={onOpenPost} + /> + ) : null} ); } @@ -4621,7 +4638,8 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean
    - {destination === "admin" ? : null}