import { Button, Popover } from "../../../../components/ui";
import { Box, Text, useUiCapabilities } from "../../../../ui";
import { TextAttributes } from "../../../../ui";
import { colors } from "../../../../theme/colors";
import type { ChatUserSummary, PublicPortfolioAnalytics } from "../../../../api-client";
import { formatNumber } from "../../../../utils/format";
import { truncateWithEllipsis } from "../../../../utils/text-wrap";
export const PROFILE_POPOVER_CLOSE_DELAY_MS = 40;
function hasPortfolioAnalytics(analytics: PublicPortfolioAnalytics | null | undefined): boolean {
return Boolean(
analytics
&& (
analytics.oneYearReturn != null
|| analytics.spyBeta != null
),
);
}
export function hasPublicChatProfileInfo(user: ChatUserSummary): boolean {
if (user.profilePublic === false) return false;
return Boolean(user.bio?.trim() || user.title?.trim() || user.company?.trim() || hasPortfolioAnalytics(user.portfolioAnalytics));
}
export function hasChatProfileDetails(user: ChatUserSummary): boolean {
return Boolean(user.bio?.trim() || user.title?.trim() || user.company?.trim());
}
export function shouldOfferChatProfileSetup(user: ChatUserSummary, isOwnProfile: boolean): boolean {
return isOwnProfile && !hasChatProfileDetails(user);
}
function formatSignedPercent(value: number): string {
const percent = value * 100;
return `${percent >= 0 ? "+" : ""}${formatNumber(percent, 2)}%`;
}
type AnalyticsMetric = {
id: "one-year" | "beta";
label: string;
value: string;
rawValue: number;
};
function analyticsValueColor(id: string, value: number): string {
if (id === "one-year") {
if (value > 0) return colors.positive;
if (value < 0) return colors.negative;
}
return colors.warning;
}
function analyticsMetrics(analytics: PublicPortfolioAnalytics): AnalyticsMetric[] {
return [
analytics.oneYearReturn != null
? {
id: "one-year",
label: "1Y",
value: formatSignedPercent(analytics.oneYearReturn),
rawValue: analytics.oneYearReturn,
}
: null,
analytics.spyBeta != null
? {
id: "beta",
label: "Beta",
value: formatNumber(analytics.spyBeta, 2),
rawValue: analytics.spyBeta,
}
: null,
].filter((metric): metric is AnalyticsMetric => !!metric);
}
function headerMetricLabel(metric: AnalyticsMetric): string {
return metric.id === "beta" ? "Beta" : metric.label;
}
function headerMetricsNaturalWidth(metrics: AnalyticsMetric[]): number {
const metricWidth = metrics.reduce((sum, metric) => (
sum + headerMetricLabel(metric).length + 1 + metric.value.length
), 0);
return metricWidth + Math.max(0, metrics.length - 1);
}
function HeaderAnalyticsStats({
metrics,
width,
}: {
metrics: AnalyticsMetric[];
width: number;
}) {
if (metrics.length === 0 || width < 8) return null;
const gapWidth = Math.max(0, metrics.length - 1);
const naturalWidths = metrics.map((metric) => headerMetricLabel(metric).length + 1 + metric.value.length);
const availableMetricWidth = Math.max(metrics.length * 4, width - gapWidth);
let overflow = Math.max(0, naturalWidths.reduce((sum, value) => sum + value, 0) - availableMetricWidth);
const metricWidths = naturalWidths.map((naturalWidth, index) => {
const shrinkable = Math.max(0, naturalWidth - 4);
const shrink = Math.min(shrinkable, Math.ceil(overflow / (naturalWidths.length - index)));
overflow -= shrink;
return naturalWidth - shrink;
});
return (
{metrics.map((metric, index) => {
const label = headerMetricLabel(metric);
const metricWidth = metricWidths[index] ?? 4;
const labelWidth = Math.max(1, Math.min(label.length, metricWidth - 2));
const valueWidth = Math.max(1, metricWidth - labelWidth - 1);
return (
{truncateWithEllipsis(label, labelWidth)}
{truncateWithEllipsis(metric.value, valueWidth)}
);
})}
);
}
export function UserProfilePopover({
user,
width,
onClose,
onKeepOpen,
isOwnProfile = false,
onSetUpProfile,
}: {
user: ChatUserSummary;
width: number;
onClose: () => void;
onKeepOpen: () => void;
isOwnProfile?: boolean;
onSetUpProfile?: () => void;
}) {
const popoverWidth = Math.max(24, Math.min(38, width - 4));
const meta = [user.title, user.company].filter(Boolean).join(" ยท ");
const bio = user.bio?.trim();
const analytics = user.portfolioAnalytics;
const metrics = analytics ? analyticsMetrics(analytics) : [];
const headerWidth = Math.max(1, popoverWidth - 2);
const maxStatsWidth = Math.max(0, headerWidth - 8);
const statsWidth = metrics.length > 0 && maxStatsWidth >= 8
? Math.min(headerMetricsNaturalWidth(metrics), maxStatsWidth)
: 0;
const usernameWidth = Math.max(1, headerWidth - (statsWidth > 0 ? statsWidth + 1 : 0));
const showSetupAction = shouldOfferChatProfileSetup(user, isOwnProfile) && !!onSetUpProfile;
const { nativePaneChrome } = useUiCapabilities();
const content = (
<>
{truncateWithEllipsis(user.username ? `@${user.username}` : user.displayName, usernameWidth)}
{statsWidth > 0 ? (
<>
>
) : null}
{meta ? {truncateWithEllipsis(meta, popoverWidth - 2)} : null}
{bio ? (
{bio}
) : null}
{showSetupAction ? (
) : null}
>
);
if (nativePaneChrome) {
// The kit popover, anchored to the chat pane's top-right corner where the
// terminal card sits. It stays open while the pointer is over it.
return (
{ if (!open) onClose(); }}
trigger={}
placement="bottom-end"
minWidth={0}
focusOnOpen={false}
label="User profile"
>
{content}
);
}
return (
{content}
);
}