From 112c0b9d60e28aba8ba4240329bc8bb24b7c149f Mon Sep 17 00:00:00 2001 From: Michael Sitarzewski Date: Mon, 22 Jun 2026 00:53:07 -0500 Subject: [PATCH] feat(web): move Copy/Export to top of the consensus report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the Copy and Export action bar from the bottom of the completed consensus report to the top, above the decision text. The export dropdown now opens downward (was upward, overlapping the report text), uses an opaque surface background (was translucent glass — text bled through), and closes on outside-click, matching the shared ExportMenu used by the thread (library) view. No markdown change needed: both the live consensus view and the library thread view already render the decision through the same shared component (remark-gfm + rehype-highlight + duh-prose), so formatting is identical. 204 Vitest tests pass, build clean. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01EkrekgzMAQko92UkjnXhHL --- .../consensus/ConsensusComplete.tsx | 96 +++++++++++-------- 1 file changed, 56 insertions(+), 40 deletions(-) diff --git a/web/src/components/consensus/ConsensusComplete.tsx b/web/src/components/consensus/ConsensusComplete.tsx index 2de7c5e..84035da 100644 --- a/web/src/components/consensus/ConsensusComplete.tsx +++ b/web/src/components/consensus/ConsensusComplete.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, useRef, useEffect } from 'react' import { GlassPanel, GlowButton, Markdown, Disclosure } from '@/components/shared' import { ConfidenceMeter } from './ConfidenceMeter' import { DissentBanner } from './DissentBanner' @@ -103,8 +103,21 @@ function downloadFile(content: string | Blob, filename: string, mimeType: string export function ConsensusComplete({ decision, confidence, rigor, dissent, cost, usage, collapsible, overview }: ConsensusCompleteProps) { const [copied, setCopied] = useState(false) const [exportOpen, setExportOpen] = useState(false) + const exportRef = useRef(null) const { question, rounds, threadId } = useConsensusStore() + // Close the export menu when clicking outside it + useEffect(() => { + if (!exportOpen) return + const handler = (e: MouseEvent) => { + if (exportRef.current && !exportRef.current.contains(e.target as Node)) { + setExportOpen(false) + } + } + document.addEventListener('mousedown', handler) + return () => document.removeEventListener('mousedown', handler) + }, [exportOpen]) + const handleCopy = async () => { await navigator.clipboard.writeText(overview ?? decision) setCopied(true) @@ -138,8 +151,50 @@ export function ConsensusComplete({ decision, confidence, rigor, dissent, cost, ) + const actions = ( +
+ + {copied ? 'Copied' : 'Copy'} + +
+ setExportOpen(!exportOpen)}> + Export + + {exportOpen && ( +
+ + + + +
+ )} +
+
+ ) + const body = ( <> + {actions} {overview ? ( <> {overview} @@ -152,45 +207,6 @@ export function ConsensusComplete({ decision, confidence, rigor, dissent, cost, ) : ( {decision} )} - -
- - {copied ? 'Copied' : 'Copy'} - -
- setExportOpen(!exportOpen)}> - Export - - {exportOpen && ( -
- - - - -
- )} -
-
)