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 { Checkbox } from '@renderer/components/ui/checkbox';
|
||||
import { HoverTooltip } from '@renderer/components/ui/hover-tooltip';
|
||||
import { Input } from '@renderer/components/ui/input';
|
||||
import { Label } from '@renderer/components/ui/label';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover';
|
||||
|
|
@ -47,6 +48,7 @@ import {
|
|||
CheckCircle2,
|
||||
ChevronDown,
|
||||
Filter,
|
||||
Info,
|
||||
Search,
|
||||
Star,
|
||||
} from 'lucide-react';
|
||||
|
|
@ -819,6 +821,18 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
<span>{modelRecommendation.label}</span>
|
||||
</span>
|
||||
) : 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 && (
|
||||
<span
|
||||
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" />
|
||||
<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>
|
||||
)}
|
||||
{!hasModelIssue && modelDisabledReason && (
|
||||
|
|
@ -834,6 +858,14 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
title={modelDisabledReason}
|
||||
>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
import { RoleSelect } from '@renderer/components/team/RoleSelect';
|
||||
import { Button } from '@renderer/components/ui/button';
|
||||
import { Checkbox } from '@renderer/components/ui/checkbox';
|
||||
import { HoverTooltip } from '@renderer/components/ui/hover-tooltip';
|
||||
import { Input } from '@renderer/components/ui/input';
|
||||
import { Label } from '@renderer/components/ui/label';
|
||||
import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea';
|
||||
|
|
@ -262,6 +263,21 @@ export const MemberDraftRow = ({
|
|||
const modelHelpDescriptionId = modelTooltipText ? `member-${member.id}-model-help` : undefined;
|
||||
const modelButtonDescribedBy =
|
||||
[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 =
|
||||
!forceInheritedModelSettings && Boolean(member.providerId || member.model?.trim());
|
||||
const showSonnetExtraUsageWarning =
|
||||
|
|
@ -357,7 +373,13 @@ export const MemberDraftRow = ({
|
|||
</Button>
|
||||
) : null}
|
||||
<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
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
|
@ -382,7 +404,7 @@ export const MemberDraftRow = ({
|
|||
<AlertTriangle className="size-3.5 shrink-0 text-red-300" />
|
||||
) : null}
|
||||
</Button>
|
||||
</span>
|
||||
</HoverTooltip>
|
||||
{modelTooltipText ? (
|
||||
<span id={modelHelpDescriptionId} className="sr-only">
|
||||
{modelTooltipText}
|
||||
|
|
@ -400,34 +422,41 @@ export const MemberDraftRow = ({
|
|||
</div>
|
||||
{showWorktreeIsolationControls ? (
|
||||
<div className="space-y-0.5">
|
||||
<div
|
||||
className={cn(
|
||||
'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)]',
|
||||
worktreeIsolationDisabled && 'cursor-not-allowed opacity-50'
|
||||
)}
|
||||
<HoverTooltip
|
||||
as="div"
|
||||
content={worktreeIsolationDescription}
|
||||
title={worktreeIsolationDescription}
|
||||
aria-describedby={worktreeIsolationDescriptionId}
|
||||
className="shrink-0"
|
||||
contentClassName="max-w-64"
|
||||
>
|
||||
<Checkbox
|
||||
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`}
|
||||
<div
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center gap-1.5 text-xs font-normal',
|
||||
worktreeIsolationDisabled && 'cursor-not-allowed'
|
||||
'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 opacity-50'
|
||||
)}
|
||||
aria-describedby={worktreeIsolationDescriptionId}
|
||||
>
|
||||
<GitBranch className="size-3.5 shrink-0" />
|
||||
<span>Worktree</span>
|
||||
</Label>
|
||||
</div>
|
||||
<Checkbox
|
||||
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(
|
||||
'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">
|
||||
{worktreeIsolationDescription}
|
||||
</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