Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Text overflow logic #4051

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions web/src/app/chat/input/LLMPopover.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import React, { useState, useEffect } from "react";
import React, {
useState,
useEffect,
useCallback,
useLayoutEffect,
} from "react";
import {
Popover,
PopoverContent,
Expand Down Expand Up @@ -28,6 +33,7 @@ import { FiAlertTriangle } from "react-icons/fi";

import { Slider } from "@/components/ui/slider";
import { useUser } from "@/components/user/UserProvider";
import { TruncatedText } from "@/components/ui/truncatedText";

interface LLMPopoverProps {
llmProviders: LLMProviderDescriptor[];
Expand Down Expand Up @@ -160,9 +166,7 @@ export default function LLMPopover({
size: 16,
className: "flex-none my-auto text-black",
})}
<span className="line-clamp-1 ">
{getDisplayNameForModel(name)}
</span>
<TruncatedText text={getDisplayNameForModel(name)} />
{(() => {
if (currentAssistant?.llm_model_version_override === name) {
return (
Expand Down
51 changes: 7 additions & 44 deletions web/src/app/chat/sessionSidebar/HistorySidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import React, {
ForwardedRef,
forwardRef,
useContext,
useState,
useCallback,
useLayoutEffect,
useRef,
} from "react";
import Link from "next/link";
import {
Expand Down Expand Up @@ -50,9 +47,9 @@ import {
} from "@dnd-kit/sortable";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { CirclePlus, CircleX, PinIcon } from "lucide-react";
import { CircleX, PinIcon } from "lucide-react";
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
import { turborepoTraceAccess } from "next/dist/build/turborepo-access-trace";
import { TruncatedText } from "@/components/ui/truncatedText";

interface HistorySidebarProps {
liveAssistant?: Persona | null;
Expand Down Expand Up @@ -101,24 +98,6 @@ const SortableAssistant: React.FC<SortableAssistantProps> = ({
...(isDragging ? { zIndex: 1000, position: "relative" as const } : {}),
};

const nameRef = useRef<HTMLParagraphElement>(null);
const hiddenNameRef = useRef<HTMLSpanElement>(null);
const [isNameTruncated, setIsNameTruncated] = useState(false);

useLayoutEffect(() => {
const checkTruncation = () => {
if (nameRef.current && hiddenNameRef.current) {
const visibleWidth = nameRef.current.offsetWidth;
const fullTextWidth = hiddenNameRef.current.offsetWidth;
setIsNameTruncated(fullTextWidth > visibleWidth);
}
};

checkTruncation();
window.addEventListener("resize", checkTruncation);
return () => window.removeEventListener("resize", checkTruncation);
}, [assistant.name]);

return (
<div
ref={setNodeRef}
Expand Down Expand Up @@ -146,27 +125,11 @@ const SortableAssistant: React.FC<SortableAssistantProps> = ({
} relative flex items-center gap-x-2 py-1 px-2 rounded-md`}
>
<AssistantIcon assistant={assistant} size={16} className="flex-none" />
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<p
ref={nameRef}
className="text-base text-left w-fit line-clamp-1 text-ellipsis text-black dark:text-[#D4D4D4]"
>
{assistant.name}
</p>
</TooltipTrigger>
{isNameTruncated && (
<TooltipContent>{assistant.name}</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
<span
ref={hiddenNameRef}
className="absolute left-[-9999px] whitespace-nowrap"
>
{assistant.name}
</span>
<TruncatedText
className="text-base mr-4 text-left w-fit line-clamp-1 text-ellipsis text-black dark:text-[#D4D4D4]"
text={assistant.name}
/>

<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
Expand Down
86 changes: 86 additions & 0 deletions web/src/components/ui/truncatedText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React, {
useState,
useRef,
useLayoutEffect,
HTMLAttributes,
} from "react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";

interface TruncatedTextProps extends HTMLAttributes<HTMLSpanElement> {
text: string;
tooltipClassName?: string;
tooltipSide?: "top" | "right" | "bottom" | "left";
tooltipSideOffset?: number;
}

/**
* Renders passed in text on a single line. If text is truncated,
* shows a tooltip on hover with the full text.
*/
export function TruncatedText({
text,
tooltipClassName,
tooltipSide = "right",
tooltipSideOffset = 5,
className = "",
...rest
}: TruncatedTextProps) {
const [isTruncated, setIsTruncated] = useState(false);
const visibleRef = useRef<HTMLSpanElement>(null);
const hiddenRef = useRef<HTMLSpanElement>(null);

useLayoutEffect(() => {
function checkTruncation() {
if (visibleRef.current && hiddenRef.current) {
const visibleWidth = visibleRef.current.offsetWidth;
const fullTextWidth = hiddenRef.current.offsetWidth;
setIsTruncated(fullTextWidth > visibleWidth);
}
}

checkTruncation();
window.addEventListener("resize", checkTruncation);
return () => window.removeEventListener("resize", checkTruncation);
}, [text]);

return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span
ref={visibleRef}
// Ensure the text can actually truncate via line-clamp or overflow
className={`line-clamp-1 break-all flex-grow ${className}`}
{...rest}
>
{text}
</span>
</TooltipTrigger>
{/* Hide offscreen to measure full text width */}
<span
ref={hiddenRef}
className="absolute left-[-9999px] whitespace-nowrap pointer-events-none"
aria-hidden="true"
>
{text}
</span>
{isTruncated && (
<TooltipContent
side={tooltipSide}
sideOffset={tooltipSideOffset}
className={tooltipClassName}
>
<p className="text-xs max-w-[200px] whitespace-normal break-words">
{text}
</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
);
}
Loading