Skip to content
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
3 changes: 2 additions & 1 deletion frontend/src/core/components/tools/FullscreenToolList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "@app/data/toolsTaxonomy";
import { ToolId } from "@app/types/toolId";
import { useToolSections } from "@app/hooks/useToolSections";
import { openUrl } from "@app/utils/urlExtensions";
import NoToolsFound from "@app/components/tools/shared/NoToolsFound";
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
import StarRoundedIcon from "@mui/icons-material/StarRounded";
Expand Down Expand Up @@ -105,7 +106,7 @@ const FullscreenToolList = ({
if (!tool.component && !tool.link && id !== "read" && id !== "multiTool")
return;
if (tool.link) {
window.open(tool.link, "_blank", "noopener,noreferrer");
openUrl(tool.link, "_blank", "noopener,noreferrer");
return;
}
onSelect(id as ToolId);
Expand Down
288 changes: 96 additions & 192 deletions frontend/src/core/components/tools/toolPicker/ToolButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ToolIcon } from "@app/components/shared/ToolIcon";
import { ToolRegistryEntry } from "@app/data/toolsTaxonomy";
import { useToolNavigation } from "@app/hooks/useToolNavigation";
import { handleUnlessSpecialClick } from "@app/utils/clickHandlers";
import { openUrl } from "@app/utils/urlExtensions";
import FitText from "@app/components/shared/FitText";
import { useHotkeys } from "@app/contexts/HotkeyContext";
import HotkeyDisplay from "@app/components/hotkeys/HotkeyDisplay";
Expand All @@ -27,45 +28,27 @@ interface ToolButtonProps {
onSelect: (id: ToolId) => void;
rounded?: boolean;
disableNavigation?: boolean;
onUnavailableClick?: () => void;
matchedSynonym?: string;
hasStars?: boolean;
/** Called when an unavailable tool is clicked; if provided, overrides the default no-op */
onUnavailableClick?: () => void;
}

const ToolButton: React.FC<ToolButtonProps> = ({
id,
tool,
isSelected,
onSelect,
rounded = false,
disableNavigation = false,
onUnavailableClick,
matchedSynonym,
hasStars = false,
onUnavailableClick,
}) => {
const { t } = useTranslation();
const { getToolNavigation } = useToolNavigation();
const { toolAvailability } = useToolWorkflow();
const { config } = useAppConfig();
const premiumEnabled = config?.premiumEnabled;
const { isFavorite, toggleFavorite, toolAvailability } = useToolWorkflow();
const disabledReason = getToolDisabledReason(
id,
tool,
toolAvailability,
premiumEnabled,
);
const isUnavailable = disabledReason !== null;
// If onUnavailableClick is provided for a non-comingSoon tool, render as "cloud-available":
// full opacity, cloud badge, normal tooltip — clicking still fires onUnavailableClick (e.g. sign-in).
const showAsCloudAvailable =
isUnavailable &&
!!onUnavailableClick &&
disabledReason !== "comingSoon" &&
disabledReason !== "selfHostedOffline";
const visuallyUnavailable = isUnavailable && !showAsCloudAvailable;
const { hotkeys } = useHotkeys();
const binding = hotkeys[id];
const { getToolNavigation } = useToolNavigation();
const fav = isFavorite(id as ToolId);

// Check if this tool will route to SaaS backend (desktop only)
const rawEndpoint = tool.operationConfig?.endpoint;
Expand All @@ -80,7 +63,7 @@ const ToolButton: React.FC<ToolButtonProps> = ({
}
if (tool.link) {
// Open external link in new tab
window.open(tool.link, "_blank", "noopener,noreferrer");
openUrl(tool.link, "_blank", "noopener,noreferrer");
return;
}
// Normal tool selection
Expand All @@ -93,109 +76,7 @@ const ToolButton: React.FC<ToolButtonProps> = ({
? getToolNavigation(id, tool)
: null;

const { key: disabledKey, fallback: disabledFallback } =
getDisabledLabel(disabledReason);
const disabledMessage = t(disabledKey, disabledFallback);

const tooltipContent = visuallyUnavailable ? (
<span>
<strong>{disabledMessage}</strong> {tool.description}
</span>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: "0.35rem" }}>
<span>{tool.description}</span>
<div
style={{
display: "flex",
alignItems: "center",
gap: "0.5rem",
fontSize: "0.75rem",
}}
>
{binding ? (
<>
<span
style={{ color: "var(--mantine-color-dimmed)", fontWeight: 500 }}
>
{t("settings.hotkeys.shortcut", "Shortcut")}
</span>
<HotkeyDisplay binding={binding} />
</>
) : (
<span
style={{
color: "var(--mantine-color-dimmed)",
fontWeight: 500,
fontStyle: "italic",
}}
>
{t("settings.hotkeys.noShortcut", "No shortcut set")}
</span>
)}
</div>
</div>
);

const buttonContent = (
<>
<ToolIcon icon={tool.icon} opacity={visuallyUnavailable ? 0.25 : 1} />
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
flex: 1,
overflow: "visible",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: "0.5rem",
width: "100%",
}}
>
<FitText
text={tool.name}
lines={1}
minimumFontScale={0.8}
as="span"
style={{
display: "inline-block",
maxWidth: "100%",
opacity: visuallyUnavailable ? 0.25 : 1,
}}
/>
{tool.versionStatus === "alpha" && (
<Badge
size="xs"
variant="light"
color="orange"
style={{ flexShrink: 0, opacity: visuallyUnavailable ? 0.25 : 1 }}
>
{t("toolPanel.alpha", "Alpha")}
</Badge>
)}
{usesCloud && !visuallyUnavailable && <CloudBadge />}
</div>
{matchedSynonym && (
<span
style={{
fontSize: "0.75rem",
color: "var(--mantine-color-dimmed)",
opacity: visuallyUnavailable ? 0.25 : 1,
marginTop: "1px",
overflow: "visible",
whiteSpace: "nowrap",
}}
>
{matchedSynonym}
</span>
)}
</div>
</>
);
const isUnavailable = toolAvailability?.[id] === "unavailable";

const handleExternalClick = (e: React.MouseEvent) => {
handleUnlessSpecialClick(e, () => handleClick(id));
Expand All @@ -210,96 +91,119 @@ const ToolButton: React.FC<ToolButtonProps> = ({
variant={isSelected ? "filled" : "subtle"}
size="sm"
radius="md"
fullWidth
justify="flex-start"
className="tool-button"
data-tour={`tool-button-${id}`}
styles={{
root: {
borderRadius: 0,
color: "var(--tools-text-and-icon-color)",
overflow: "visible",
},
label: { overflow: "visible" },
}}
className={`tool-button ${rounded ? "tool-button--rounded" : ""} ${
isSelected ? "tool-button--selected" : ""
} ${isUnavailable ? "tool-button--unavailable" : ""}`}
title={tool.name}
data-tool-id={id}
disabled={isUnavailable}
aria-label={tool.name}
>
{buttonContent}
<div className="tool-button-content">
<ToolIcon icon={tool.icon} size={20} />
<FitText
text={matchedSynonym || tool.name}
className="tool-button-label"
maxLines={1}
/>
{hasStars && <FavoriteStar isFavorite={false} onToggle={() => {}} />}
{usesCloud && <CloudBadge />}
{isUnavailable && (
<Badge
size="xs"
variant="light"
color="gray"
className="tool-button-unavailable-badge"
>
{t("common.unavailable", "Unavailable")}
</Badge>
)}
</div>
</Button>
) : tool.link && !isUnavailable ? (
// For external links, render Button as an anchor with proper href
<Button
component="a"
href={tool.link}
target="_blank"
rel="noopener noreferrer"
onClick={handleExternalClick}
variant={isSelected ? "filled" : "subtle"}
size="sm"
radius="md"
fullWidth
justify="flex-start"
className="tool-button"
data-tour={`tool-button-${id}`}
styles={{
root: {
borderRadius: 0,
color: "var(--tools-text-and-icon-color)",
overflow: "visible",
},
label: { overflow: "visible" },
}}
className={`tool-button ${rounded ? "tool-button--rounded" : ""} ${
isSelected ? "tool-button--selected" : ""
}`}
title={tool.name}
data-tool-id={id}
target="_blank"
rel="noopener noreferrer"
aria-label={tool.name}
>
{buttonContent}
<div className="tool-button-content">
<ToolIcon icon={tool.icon} size={20} />
<FitText
text={matchedSynonym || tool.name}
className="tool-button-label"
maxLines={1}
/>
{hasStars && <FavoriteStar isFavorite={false} onToggle={() => {}} />}
</div>
</Button>
) : (
// For unavailable tools, use regular button
// For normal tools without URLs
<Button
variant={isSelected ? "filled" : "subtle"}
onClick={() => handleClick(id)}
size="sm"
radius="md"
fullWidth
justify="flex-start"
className="tool-button"
aria-disabled={isUnavailable}
data-tour={`tool-button-${id}`}
styles={{
root: {
borderRadius: 0,
color: "var(--tools-text-and-icon-color)",
cursor: visuallyUnavailable ? "not-allowed" : undefined,
overflow: "visible",
},
label: { overflow: "visible" },
}}
className={`tool-button ${rounded ? "tool-button--rounded" : ""} ${
isSelected ? "tool-button--selected" : ""
} ${isUnavailable ? "tool-button--unavailable" : ""}`}
title={tool.name}
data-tool-id={id}
disabled={isUnavailable}
aria-label={tool.name}
>
{buttonContent}
<div className="tool-button-content">
<ToolIcon icon={tool.icon} size={20} />
<FitText
text={matchedSynonym || tool.name}
className="tool-button-label"
maxLines={1}
/>
{hasStars && <FavoriteStar isFavorite={false} onToggle={() => {}} />}
{usesCloud && <CloudBadge />}
{isUnavailable && (
<Badge
size="xs"
variant="light"
color="gray"
className="tool-button-unavailable-badge"
>
{t("common.unavailable", "Unavailable")}
</Badge>
)}
</div>
</Button>
);

const star =
hasStars && !visuallyUnavailable ? (
<FavoriteStar
isFavorite={fav}
onToggle={() => toggleFavorite(id as ToolId)}
className="tool-button-star"
size="xs"
/>
) : null;
const unavailableReason = isUnavailable
? getToolDisabledReason(tool, premiumEnabled)
: null;
const disabledLabel = unavailableReason
? getDisabledLabel(unavailableReason)
: null;

return (
<div className="tool-button-container">
{star}
<Tooltip
content={tooltipContent}
position="right"
arrow={true}
delay={500}
>
{buttonElement}
</Tooltip>
</div>
<Tooltip
label={disabledLabel || tool.description}
disabled={!disabledLabel && !tool.description}
position="right"
withArrow
openDelay={500}
>
{buttonElement}
</Tooltip>
);
};

export default ToolButton;
export default ToolButton;
Loading
Loading