Enhance popover positioning in RetrievalTool and SourceRow components by introducing a consistent POPOVER_GAP. Update dialog position logic to account for placement above or below the trigger element, ensuring better visibility and user experience. Refactor ToggleToolRow to utilize the new positioning logic for info dialogs.
Browse files
frontend/components/source-sidebar.tsx
CHANGED
|
@@ -41,6 +41,8 @@ type ToggleToolMeta = {
|
|
| 41 |
description?: string;
|
| 42 |
};
|
| 43 |
|
|
|
|
|
|
|
| 44 |
const TOGGLE_TOOL_META: Record<string, ToggleToolMeta> = {
|
| 45 |
web_search: {
|
| 46 |
icon: Globe,
|
|
@@ -225,7 +227,7 @@ function RetrievalTool({
|
|
| 225 |
const [infoOpen, setInfoOpen] = useState(false);
|
| 226 |
const infoRef = useRef<HTMLDivElement>(null);
|
| 227 |
const [dialogPos, setDialogPos] = useState<
|
| 228 |
-
{ top: number; left: number; width: number } | null
|
| 229 |
>(null);
|
| 230 |
|
| 231 |
useEffect(() => {
|
|
@@ -268,9 +270,10 @@ function RetrievalTool({
|
|
| 268 |
const spaceBelow = window.innerHeight - trigger.bottom;
|
| 269 |
const openAbove = spaceBelow < estimatedHeight + 16 && trigger.top > spaceBelow;
|
| 270 |
setDialogPos({
|
| 271 |
-
top: openAbove ? trigger.top
|
| 272 |
left: Math.max(12, trigger.right - width),
|
| 273 |
width,
|
|
|
|
| 274 |
});
|
| 275 |
}
|
| 276 |
|
|
@@ -354,6 +357,10 @@ function RetrievalTool({
|
|
| 354 |
top: dialogPos.top,
|
| 355 |
left: dialogPos.left,
|
| 356 |
width: dialogPos.width,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
zIndex: 50,
|
| 358 |
}}
|
| 359 |
className="rounded-[0.9rem] border border-[var(--line-strong)] bg-[var(--surface-strong)] p-3 shadow-[0_12px_32px_rgba(0,0,0,0.18)] backdrop-blur-md"
|
|
@@ -516,7 +523,7 @@ function SourceRow({
|
|
| 516 |
const popover = getPopoverInfo(source);
|
| 517 |
const rowRef = useRef<HTMLDivElement>(null);
|
| 518 |
const [dialogPos, setDialogPos] = useState<
|
| 519 |
-
{ top: number; left: number; width: number } | null
|
| 520 |
>(null);
|
| 521 |
|
| 522 |
useLayoutEffect(() => {
|
|
@@ -530,9 +537,10 @@ function SourceRow({
|
|
| 530 |
const spaceBelow = window.innerHeight - row.bottom;
|
| 531 |
const openAbove = spaceBelow < estimatedHeight + 16 && row.top > spaceBelow;
|
| 532 |
setDialogPos({
|
| 533 |
-
top: openAbove ? row.top
|
| 534 |
left: row.left,
|
| 535 |
width: row.width,
|
|
|
|
| 536 |
});
|
| 537 |
}
|
| 538 |
recompute();
|
|
@@ -604,6 +612,10 @@ function SourceRow({
|
|
| 604 |
top: dialogPos.top,
|
| 605 |
left: dialogPos.left,
|
| 606 |
width: dialogPos.width,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 607 |
zIndex: 50,
|
| 608 |
}}
|
| 609 |
className="rounded-[0.9rem] border border-[var(--line-strong)] bg-[var(--surface-strong)] p-3 shadow-[0_12px_32px_rgba(0,0,0,0.18)] backdrop-blur-md"
|
|
@@ -649,8 +661,41 @@ function ToggleToolRow({
|
|
| 649 |
const meta = TOGGLE_TOOL_META[tool.key];
|
| 650 |
const Icon = meta?.icon ?? Globe;
|
| 651 |
const description = meta?.description;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 652 |
return (
|
| 653 |
-
<div className="relative">
|
| 654 |
<div
|
| 655 |
className={clsx(
|
| 656 |
"flex items-center gap-1 rounded-[0.9rem] border pl-2 pr-1 py-2 transition",
|
|
@@ -706,16 +751,31 @@ function ToggleToolRow({
|
|
| 706 |
<span className="h-5 w-5 shrink-0" aria-hidden />
|
| 707 |
)}
|
| 708 |
</div>
|
| 709 |
-
{description && infoOpen
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 719 |
</div>
|
| 720 |
);
|
| 721 |
}
|
|
|
|
| 41 |
description?: string;
|
| 42 |
};
|
| 43 |
|
| 44 |
+
const POPOVER_GAP = 6;
|
| 45 |
+
|
| 46 |
const TOGGLE_TOOL_META: Record<string, ToggleToolMeta> = {
|
| 47 |
web_search: {
|
| 48 |
icon: Globe,
|
|
|
|
| 227 |
const [infoOpen, setInfoOpen] = useState(false);
|
| 228 |
const infoRef = useRef<HTMLDivElement>(null);
|
| 229 |
const [dialogPos, setDialogPos] = useState<
|
| 230 |
+
{ top: number; left: number; width: number; placement: "above" | "below" } | null
|
| 231 |
>(null);
|
| 232 |
|
| 233 |
useEffect(() => {
|
|
|
|
| 270 |
const spaceBelow = window.innerHeight - trigger.bottom;
|
| 271 |
const openAbove = spaceBelow < estimatedHeight + 16 && trigger.top > spaceBelow;
|
| 272 |
setDialogPos({
|
| 273 |
+
top: openAbove ? trigger.top : trigger.bottom + POPOVER_GAP,
|
| 274 |
left: Math.max(12, trigger.right - width),
|
| 275 |
width,
|
| 276 |
+
placement: openAbove ? "above" : "below",
|
| 277 |
});
|
| 278 |
}
|
| 279 |
|
|
|
|
| 357 |
top: dialogPos.top,
|
| 358 |
left: dialogPos.left,
|
| 359 |
width: dialogPos.width,
|
| 360 |
+
transform:
|
| 361 |
+
dialogPos.placement === "above"
|
| 362 |
+
? `translateY(calc(-100% - ${POPOVER_GAP}px))`
|
| 363 |
+
: undefined,
|
| 364 |
zIndex: 50,
|
| 365 |
}}
|
| 366 |
className="rounded-[0.9rem] border border-[var(--line-strong)] bg-[var(--surface-strong)] p-3 shadow-[0_12px_32px_rgba(0,0,0,0.18)] backdrop-blur-md"
|
|
|
|
| 523 |
const popover = getPopoverInfo(source);
|
| 524 |
const rowRef = useRef<HTMLDivElement>(null);
|
| 525 |
const [dialogPos, setDialogPos] = useState<
|
| 526 |
+
{ top: number; left: number; width: number; placement: "above" | "below" } | null
|
| 527 |
>(null);
|
| 528 |
|
| 529 |
useLayoutEffect(() => {
|
|
|
|
| 537 |
const spaceBelow = window.innerHeight - row.bottom;
|
| 538 |
const openAbove = spaceBelow < estimatedHeight + 16 && row.top > spaceBelow;
|
| 539 |
setDialogPos({
|
| 540 |
+
top: openAbove ? row.top : row.bottom + POPOVER_GAP,
|
| 541 |
left: row.left,
|
| 542 |
width: row.width,
|
| 543 |
+
placement: openAbove ? "above" : "below",
|
| 544 |
});
|
| 545 |
}
|
| 546 |
recompute();
|
|
|
|
| 612 |
top: dialogPos.top,
|
| 613 |
left: dialogPos.left,
|
| 614 |
width: dialogPos.width,
|
| 615 |
+
transform:
|
| 616 |
+
dialogPos.placement === "above"
|
| 617 |
+
? `translateY(calc(-100% - ${POPOVER_GAP}px))`
|
| 618 |
+
: undefined,
|
| 619 |
zIndex: 50,
|
| 620 |
}}
|
| 621 |
className="rounded-[0.9rem] border border-[var(--line-strong)] bg-[var(--surface-strong)] p-3 shadow-[0_12px_32px_rgba(0,0,0,0.18)] backdrop-blur-md"
|
|
|
|
| 661 |
const meta = TOGGLE_TOOL_META[tool.key];
|
| 662 |
const Icon = meta?.icon ?? Globe;
|
| 663 |
const description = meta?.description;
|
| 664 |
+
const rowRef = useRef<HTMLDivElement>(null);
|
| 665 |
+
const [dialogPos, setDialogPos] = useState<
|
| 666 |
+
{ top: number; left: number; width: number; placement: "above" | "below" } | null
|
| 667 |
+
>(null);
|
| 668 |
+
|
| 669 |
+
useLayoutEffect(() => {
|
| 670 |
+
if (!infoOpen || !rowRef.current) {
|
| 671 |
+
return;
|
| 672 |
+
}
|
| 673 |
+
|
| 674 |
+
function recompute() {
|
| 675 |
+
if (!rowRef.current) return;
|
| 676 |
+
const row = rowRef.current.getBoundingClientRect();
|
| 677 |
+
const estimatedHeight = 92;
|
| 678 |
+
const spaceAbove = row.top;
|
| 679 |
+
const openAbove = spaceAbove > estimatedHeight + 12;
|
| 680 |
+
setDialogPos({
|
| 681 |
+
top: openAbove ? row.top : row.bottom + POPOVER_GAP,
|
| 682 |
+
left: row.left,
|
| 683 |
+
width: row.width,
|
| 684 |
+
placement: openAbove ? "above" : "below",
|
| 685 |
+
});
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
recompute();
|
| 689 |
+
window.addEventListener("resize", recompute);
|
| 690 |
+
window.addEventListener("scroll", recompute, true);
|
| 691 |
+
return () => {
|
| 692 |
+
window.removeEventListener("resize", recompute);
|
| 693 |
+
window.removeEventListener("scroll", recompute, true);
|
| 694 |
+
};
|
| 695 |
+
}, [infoOpen]);
|
| 696 |
+
|
| 697 |
return (
|
| 698 |
+
<div ref={rowRef} className="relative">
|
| 699 |
<div
|
| 700 |
className={clsx(
|
| 701 |
"flex items-center gap-1 rounded-[0.9rem] border pl-2 pr-1 py-2 transition",
|
|
|
|
| 751 |
<span className="h-5 w-5 shrink-0" aria-hidden />
|
| 752 |
)}
|
| 753 |
</div>
|
| 754 |
+
{description && infoOpen && dialogPos && typeof document !== "undefined"
|
| 755 |
+
? createPortal(
|
| 756 |
+
<div
|
| 757 |
+
role="dialog"
|
| 758 |
+
data-toggle-tool-popover="true"
|
| 759 |
+
style={{
|
| 760 |
+
position: "fixed",
|
| 761 |
+
top: dialogPos.top,
|
| 762 |
+
left: dialogPos.left,
|
| 763 |
+
width: dialogPos.width,
|
| 764 |
+
transform:
|
| 765 |
+
dialogPos.placement === "above"
|
| 766 |
+
? `translateY(calc(-100% - ${POPOVER_GAP}px))`
|
| 767 |
+
: undefined,
|
| 768 |
+
zIndex: 80,
|
| 769 |
+
}}
|
| 770 |
+
className="rounded-[0.9rem] border border-[var(--line-strong)] bg-[var(--surface-strong)] p-3 shadow-[0_12px_32px_rgba(0,0,0,0.18)] backdrop-blur-md"
|
| 771 |
+
>
|
| 772 |
+
<p className="text-[12px] leading-[1.45] text-[var(--ink)]">
|
| 773 |
+
{description}
|
| 774 |
+
</p>
|
| 775 |
+
</div>,
|
| 776 |
+
document.body,
|
| 777 |
+
)
|
| 778 |
+
: null}
|
| 779 |
</div>
|
| 780 |
);
|
| 781 |
}
|