omarsol commited on
Commit
62d6725
·
1 Parent(s): 28083e8

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 - estimatedHeight - 8 : trigger.bottom + 8,
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 - estimatedHeight - 4 : row.bottom + 4,
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
- <div
711
- role="dialog"
712
- className="absolute left-0 right-0 bottom-full z-20 mb-1 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"
713
- >
714
- <p className="text-[12px] leading-[1.45] text-[var(--ink)]">
715
- {description}
716
- </p>
717
- </div>
718
- ) : null}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
  }