fix(ui): restore safe model tooltips
This commit is contained in:
parent
a3a286c652
commit
c1a2ccc0a7
3 changed files with 167 additions and 25 deletions
|
|
@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
|
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
|
||||||
import { Checkbox } from '@renderer/components/ui/checkbox';
|
import { Checkbox } from '@renderer/components/ui/checkbox';
|
||||||
|
import { HoverTooltip } from '@renderer/components/ui/hover-tooltip';
|
||||||
import { Input } from '@renderer/components/ui/input';
|
import { Input } from '@renderer/components/ui/input';
|
||||||
import { Label } from '@renderer/components/ui/label';
|
import { Label } from '@renderer/components/ui/label';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover';
|
||||||
|
|
@ -47,6 +48,7 @@ import {
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Filter,
|
Filter,
|
||||||
|
Info,
|
||||||
Search,
|
Search,
|
||||||
Star,
|
Star,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
@ -819,6 +821,18 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
<span>{modelRecommendation.label}</span>
|
<span>{modelRecommendation.label}</span>
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
{opt.value === '' ? (
|
||||||
|
<span className="flex items-center justify-center gap-1">
|
||||||
|
<HoverTooltip
|
||||||
|
content={defaultModelTooltip}
|
||||||
|
title={defaultModelTooltip}
|
||||||
|
stopClickPropagation
|
||||||
|
contentClassName="max-w-[240px]"
|
||||||
|
>
|
||||||
|
<Info className="size-3 shrink-0 opacity-45 transition-opacity hover:opacity-75" />
|
||||||
|
</HoverTooltip>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
{hasModelIssue && (
|
{hasModelIssue && (
|
||||||
<span
|
<span
|
||||||
className="flex items-center justify-center gap-1 text-[10px] font-normal text-red-300"
|
className="flex items-center justify-center gap-1 text-[10px] font-normal text-red-300"
|
||||||
|
|
@ -826,6 +840,16 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
>
|
>
|
||||||
<AlertTriangle className="size-3 shrink-0" />
|
<AlertTriangle className="size-3 shrink-0" />
|
||||||
<span>{modelUnavailableReason ? 'Unavailable' : 'Issue'}</span>
|
<span>{modelUnavailableReason ? 'Unavailable' : 'Issue'}</span>
|
||||||
|
{modelStatusMessage ? (
|
||||||
|
<HoverTooltip
|
||||||
|
content={modelStatusMessage}
|
||||||
|
title={modelStatusMessage}
|
||||||
|
stopClickPropagation
|
||||||
|
contentClassName="max-w-[240px]"
|
||||||
|
>
|
||||||
|
<Info className="size-3 shrink-0 opacity-55 transition-opacity hover:opacity-85" />
|
||||||
|
</HoverTooltip>
|
||||||
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!hasModelIssue && modelDisabledReason && (
|
{!hasModelIssue && modelDisabledReason && (
|
||||||
|
|
@ -834,6 +858,14 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
title={modelDisabledReason}
|
title={modelDisabledReason}
|
||||||
>
|
>
|
||||||
<span>{TEAM_MODEL_UI_DISABLED_BADGE_LABEL}</span>
|
<span>{TEAM_MODEL_UI_DISABLED_BADGE_LABEL}</span>
|
||||||
|
<HoverTooltip
|
||||||
|
content={modelDisabledReason}
|
||||||
|
title={modelDisabledReason}
|
||||||
|
stopClickPropagation
|
||||||
|
contentClassName="max-w-[240px]"
|
||||||
|
>
|
||||||
|
<Info className="size-3 shrink-0 opacity-45 transition-opacity hover:opacity-75" />
|
||||||
|
</HoverTooltip>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
import { RoleSelect } from '@renderer/components/team/RoleSelect';
|
import { RoleSelect } from '@renderer/components/team/RoleSelect';
|
||||||
import { Button } from '@renderer/components/ui/button';
|
import { Button } from '@renderer/components/ui/button';
|
||||||
import { Checkbox } from '@renderer/components/ui/checkbox';
|
import { Checkbox } from '@renderer/components/ui/checkbox';
|
||||||
|
import { HoverTooltip } from '@renderer/components/ui/hover-tooltip';
|
||||||
import { Input } from '@renderer/components/ui/input';
|
import { Input } from '@renderer/components/ui/input';
|
||||||
import { Label } from '@renderer/components/ui/label';
|
import { Label } from '@renderer/components/ui/label';
|
||||||
import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea';
|
import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea';
|
||||||
|
|
@ -262,6 +263,21 @@ export const MemberDraftRow = ({
|
||||||
const modelHelpDescriptionId = modelTooltipText ? `member-${member.id}-model-help` : undefined;
|
const modelHelpDescriptionId = modelTooltipText ? `member-${member.id}-model-help` : undefined;
|
||||||
const modelButtonDescribedBy =
|
const modelButtonDescribedBy =
|
||||||
[modelIssueDescriptionId, modelHelpDescriptionId].filter(Boolean).join(' ') || undefined;
|
[modelIssueDescriptionId, modelHelpDescriptionId].filter(Boolean).join(' ') || undefined;
|
||||||
|
const modelButtonTooltipContent =
|
||||||
|
currentModelIssueText || modelTooltipText ? (
|
||||||
|
<>
|
||||||
|
{currentModelIssueText ? (
|
||||||
|
<span className="block text-red-300">{currentModelIssueText}</span>
|
||||||
|
) : null}
|
||||||
|
{modelTooltipText ? (
|
||||||
|
<span
|
||||||
|
className={cn('block', currentModelIssueText && 'mt-1 border-t border-white/10 pt-1')}
|
||||||
|
>
|
||||||
|
{modelTooltipText}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : null;
|
||||||
const hasCustomProviderOrModel =
|
const hasCustomProviderOrModel =
|
||||||
!forceInheritedModelSettings && Boolean(member.providerId || member.model?.trim());
|
!forceInheritedModelSettings && Boolean(member.providerId || member.model?.trim());
|
||||||
const showSonnetExtraUsageWarning =
|
const showSonnetExtraUsageWarning =
|
||||||
|
|
@ -357,7 +373,13 @@ export const MemberDraftRow = ({
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="w-full min-w-0 space-y-1 sm:w-[150px] sm:min-w-[150px]">
|
<div className="w-full min-w-0 space-y-1 sm:w-[150px] sm:min-w-[150px]">
|
||||||
<span className="inline-flex w-full" title={modelButtonTitle}>
|
<HoverTooltip
|
||||||
|
content={modelButtonTooltipContent}
|
||||||
|
title={modelButtonTitle}
|
||||||
|
disabled={!modelButtonTooltipContent}
|
||||||
|
className="w-full"
|
||||||
|
contentClassName="max-w-64"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -382,7 +404,7 @@ export const MemberDraftRow = ({
|
||||||
<AlertTriangle className="size-3.5 shrink-0 text-red-300" />
|
<AlertTriangle className="size-3.5 shrink-0 text-red-300" />
|
||||||
) : null}
|
) : null}
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</HoverTooltip>
|
||||||
{modelTooltipText ? (
|
{modelTooltipText ? (
|
||||||
<span id={modelHelpDescriptionId} className="sr-only">
|
<span id={modelHelpDescriptionId} className="sr-only">
|
||||||
{modelTooltipText}
|
{modelTooltipText}
|
||||||
|
|
@ -400,34 +422,41 @@ export const MemberDraftRow = ({
|
||||||
</div>
|
</div>
|
||||||
{showWorktreeIsolationControls ? (
|
{showWorktreeIsolationControls ? (
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<div
|
<HoverTooltip
|
||||||
className={cn(
|
as="div"
|
||||||
'flex h-8 shrink-0 cursor-pointer items-center gap-1.5 rounded-md border border-[var(--color-border)] px-2 text-xs text-[var(--color-text-secondary)]',
|
content={worktreeIsolationDescription}
|
||||||
worktreeIsolationDisabled && 'cursor-not-allowed opacity-50'
|
|
||||||
)}
|
|
||||||
title={worktreeIsolationDescription}
|
title={worktreeIsolationDescription}
|
||||||
aria-describedby={worktreeIsolationDescriptionId}
|
className="shrink-0"
|
||||||
|
contentClassName="max-w-64"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<div
|
||||||
id={`member-${member.id}-worktree-isolation`}
|
|
||||||
checked={member.isolation === 'worktree'}
|
|
||||||
disabled={worktreeIsolationDisabled}
|
|
||||||
aria-describedby={worktreeIsolationDescriptionId}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
onWorktreeIsolationChange?.(member.id, checked === true)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Label
|
|
||||||
htmlFor={`member-${member.id}-worktree-isolation`}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex cursor-pointer items-center gap-1.5 text-xs font-normal',
|
'flex h-8 cursor-pointer items-center gap-1.5 rounded-md border border-[var(--color-border)] px-2 text-xs text-[var(--color-text-secondary)]',
|
||||||
worktreeIsolationDisabled && 'cursor-not-allowed'
|
worktreeIsolationDisabled && 'cursor-not-allowed opacity-50'
|
||||||
)}
|
)}
|
||||||
|
aria-describedby={worktreeIsolationDescriptionId}
|
||||||
>
|
>
|
||||||
<GitBranch className="size-3.5 shrink-0" />
|
<Checkbox
|
||||||
<span>Worktree</span>
|
id={`member-${member.id}-worktree-isolation`}
|
||||||
</Label>
|
checked={member.isolation === 'worktree'}
|
||||||
</div>
|
disabled={worktreeIsolationDisabled}
|
||||||
|
aria-describedby={worktreeIsolationDescriptionId}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onWorktreeIsolationChange?.(member.id, checked === true)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor={`member-${member.id}-worktree-isolation`}
|
||||||
|
className={cn(
|
||||||
|
'flex cursor-pointer items-center gap-1.5 text-xs font-normal',
|
||||||
|
worktreeIsolationDisabled && 'cursor-not-allowed'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<GitBranch className="size-3.5 shrink-0" />
|
||||||
|
<span>Worktree</span>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</HoverTooltip>
|
||||||
<span id={worktreeIsolationDescriptionId} className="sr-only">
|
<span id={worktreeIsolationDescriptionId} className="sr-only">
|
||||||
{worktreeIsolationDescription}
|
{worktreeIsolationDescription}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
81
src/renderer/components/ui/hover-tooltip.tsx
Normal file
81
src/renderer/components/ui/hover-tooltip.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@renderer/lib/utils';
|
||||||
|
|
||||||
|
type HoverTooltipSide = 'top' | 'bottom';
|
||||||
|
type HoverTooltipAlign = 'start' | 'center' | 'end';
|
||||||
|
|
||||||
|
interface HoverTooltipProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
content: React.ReactNode;
|
||||||
|
align?: HoverTooltipAlign;
|
||||||
|
as?: 'span' | 'div';
|
||||||
|
className?: string;
|
||||||
|
contentClassName?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
side?: HoverTooltipSide;
|
||||||
|
stopClickPropagation?: boolean;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sideClassBySide: Record<HoverTooltipSide, string> = {
|
||||||
|
top: 'bottom-full mb-2',
|
||||||
|
bottom: 'top-full mt-2',
|
||||||
|
};
|
||||||
|
|
||||||
|
const alignClassByAlign: Record<HoverTooltipAlign, string> = {
|
||||||
|
start: 'left-0',
|
||||||
|
center: 'left-1/2 -translate-x-1/2',
|
||||||
|
end: 'right-0',
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTooltipContent = (content: React.ReactNode): React.JSX.Element => {
|
||||||
|
return typeof content === 'string' ? (
|
||||||
|
<span className="whitespace-pre-line">{content}</span>
|
||||||
|
) : (
|
||||||
|
<span>{content}</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HoverTooltip = ({
|
||||||
|
children,
|
||||||
|
content,
|
||||||
|
align = 'center',
|
||||||
|
as = 'span',
|
||||||
|
className,
|
||||||
|
contentClassName,
|
||||||
|
disabled = false,
|
||||||
|
side = 'top',
|
||||||
|
stopClickPropagation = false,
|
||||||
|
title,
|
||||||
|
}: Readonly<HoverTooltipProps>): React.JSX.Element => {
|
||||||
|
const TooltipWrapper = as;
|
||||||
|
|
||||||
|
if (disabled || !content) {
|
||||||
|
return <TooltipWrapper className={className}>{children}</TooltipWrapper>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipWrapper
|
||||||
|
className={cn('group/hover-tooltip relative inline-flex min-w-0', className)}
|
||||||
|
title={title}
|
||||||
|
onClick={
|
||||||
|
stopClickPropagation ? (event: React.MouseEvent) => event.stopPropagation() : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn(
|
||||||
|
'pointer-events-none absolute z-[80] w-max max-w-72 rounded-md border border-[var(--color-border)] bg-[var(--color-surface-raised)] px-2.5 py-1.5 text-left text-xs font-normal leading-relaxed text-[var(--color-text-secondary)] opacity-0 shadow-lg ring-1 ring-black/5 transition-opacity duration-100',
|
||||||
|
'group-focus-within/hover-tooltip:opacity-100 group-hover/hover-tooltip:opacity-100',
|
||||||
|
sideClassBySide[side],
|
||||||
|
alignClassByAlign[align],
|
||||||
|
contentClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{renderTooltipContent(content)}
|
||||||
|
</span>
|
||||||
|
</TooltipWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue