diff --git a/frontend/src/core/components/tools/fullscreen/CompactToolItem.tsx b/frontend/src/core/components/tools/fullscreen/CompactToolItem.tsx index 648f79267..e05cc36a1 100644 --- a/frontend/src/core/components/tools/fullscreen/CompactToolItem.tsx +++ b/frontend/src/core/components/tools/fullscreen/CompactToolItem.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Text } from '@mantine/core'; +import { Text, Badge } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { Tooltip } from '@app/components/shared/Tooltip'; import HotkeyDisplay from '@app/components/hotkeys/HotkeyDisplay'; @@ -17,10 +17,14 @@ interface CompactToolItemProps { const CompactToolItem: React.FC = ({ id, tool, isSelected, onClick, tooltipPortalTarget }) => { const { t } = useTranslation(); - const { binding, isFav, toggleFavorite, disabled } = useToolMeta(id, tool); + const { binding, isFav, toggleFavorite, disabled, premiumEnabled } = useToolMeta(id, tool); const categoryColor = getSubcategoryColor(tool.subcategoryId); const iconBg = getIconBackground(categoryColor, false); const iconClasses = 'tool-panel__fullscreen-list-icon'; + + // Determine why tool is disabled for tooltip content + const isUnavailable = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool'; + const requiresPremiumButNotEnabled = tool.requiresPremium === true && premiumEnabled !== true; let iconNode: React.ReactNode = null; if (React.isValidElement<{ style?: React.CSSProperties }>(tool.icon)) { @@ -57,9 +61,20 @@ const CompactToolItem: React.FC = ({ id, tool, isSelected, ) : null} - - {tool.name} - +
+ + {tool.name} + + {tool.versionStatus === 'alpha' && ( + + {t('toolPanel.alpha', 'Alpha')} + + )} +
{!disabled && (
@@ -73,11 +88,22 @@ const CompactToolItem: React.FC = ({ id, tool, isSelected, ); - const tooltipContent = disabled - ? ( - {t('toolPanel.fullscreen.comingSoon', 'Coming soon:')} {tool.description} - ) - : ( + // Determine tooltip content based on disabled reason + let tooltipContent: React.ReactNode; + if (requiresPremiumButNotEnabled) { + tooltipContent = ( + + {t('toolPanel.premiumFeature', 'Premium feature:')} {tool.description} + + ); + } else if (isUnavailable) { + tooltipContent = ( + + {t('toolPanel.fullscreen.comingSoon', 'Coming soon:')} {tool.description} + + ); + } else { + tooltipContent = (
{tool.description} {binding && ( @@ -90,6 +116,7 @@ const CompactToolItem: React.FC = ({ id, tool, isSelected, )}
); + } return ( = ({ id, tool, isSelecte ) : null} - - {tool.name} - +
+ + {tool.name} + + {tool.versionStatus === 'alpha' && ( + + {/* we can add more translations for different badges in future, like beta, etc. */} + {t('toolPanel.alpha', 'Alpha')} + + )} +
{tool.description} diff --git a/frontend/src/core/components/tools/fullscreen/shared.ts b/frontend/src/core/components/tools/fullscreen/shared.ts index adaf36098..7713e65f0 100644 --- a/frontend/src/core/components/tools/fullscreen/shared.ts +++ b/frontend/src/core/components/tools/fullscreen/shared.ts @@ -2,6 +2,7 @@ import { useHotkeys } from '@app/contexts/HotkeyContext'; import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; import { ToolRegistryEntry } from '@app/data/toolsTaxonomy'; import { ToolId } from '@app/types/toolId'; +import { useAppConfig } from '@app/contexts/AppConfigContext'; export const getItemClasses = (isDetailed: boolean): string => { return isDetailed ? 'tool-panel__fullscreen-item--detailed' : ''; @@ -22,23 +23,32 @@ export const getIconStyle = (): Record => { return {}; }; -export const isToolDisabled = (id: string, tool: ToolRegistryEntry): boolean => { - return !tool.component && !tool.link && id !== 'read' && id !== 'multiTool'; +export const isToolDisabled = (id: string, tool: ToolRegistryEntry, premiumEnabled?: boolean): boolean => { + // Check if tool is unavailable (no component and not a link) + const isUnavailable = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool'; + + // Check if tool requires premium but premium is not enabled + const requiresPremiumButNotEnabled = tool.requiresPremium === true && premiumEnabled !== true; + + return isUnavailable || requiresPremiumButNotEnabled; }; export function useToolMeta(id: string, tool: ToolRegistryEntry) { const { hotkeys } = useHotkeys(); const { isFavorite, toggleFavorite } = useToolWorkflow(); + const { config } = useAppConfig(); + const premiumEnabled = config?.premiumEnabled; const isFav = isFavorite(id as ToolId); const binding = hotkeys[id as ToolId]; - const disabled = isToolDisabled(id, tool); + const disabled = isToolDisabled(id, tool, premiumEnabled); return { binding, isFav, toggleFavorite: () => toggleFavorite(id as ToolId), disabled, + premiumEnabled, }; } diff --git a/frontend/src/core/components/tools/toolPicker/ToolButton.tsx b/frontend/src/core/components/tools/toolPicker/ToolButton.tsx index 5b756c8ba..77c2dfdc3 100644 --- a/frontend/src/core/components/tools/toolPicker/ToolButton.tsx +++ b/frontend/src/core/components/tools/toolPicker/ToolButton.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Button } from "@mantine/core"; +import { Button, Badge } from "@mantine/core"; import { useTranslation } from "react-i18next"; import { Tooltip } from "@app/components/shared/Tooltip"; import { ToolIcon } from "@app/components/shared/ToolIcon"; @@ -12,6 +12,7 @@ import HotkeyDisplay from "@app/components/hotkeys/HotkeyDisplay"; import FavoriteStar from "@app/components/tools/toolPicker/FavoriteStar"; import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext"; import { ToolId } from "@app/types/toolId"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; interface ToolButtonProps { id: ToolId; @@ -26,8 +27,15 @@ interface ToolButtonProps { const ToolButton: React.FC = ({ id, tool, isSelected, onSelect, disableNavigation = false, matchedSynonym, hasStars = false }) => { const { t } = useTranslation(); - // Special case: read and multiTool are navigational tools that are always available + const { config } = useAppConfig(); + const premiumEnabled = config?.premiumEnabled; + + // Check if disabled due to premium requirement + const requiresPremiumButNotEnabled = tool.requiresPremium === true && premiumEnabled !== true; + // Check if tool is unavailable (no component, no link, except read/multiTool) const isUnavailable = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool'; + const isDisabled = isUnavailable || requiresPremiumButNotEnabled; + const { hotkeys } = useHotkeys(); const binding = hotkeys[id]; const { getToolNavigation } = useToolNavigation(); @@ -35,7 +43,7 @@ const ToolButton: React.FC = ({ id, tool, isSelected, onSelect, const fav = isFavorite(id as ToolId); const handleClick = (id: ToolId) => { - if (isUnavailable) return; + if (isDisabled) return; if (tool.link) { // Open external link in new tab window.open(tool.link, '_blank', 'noopener,noreferrer'); @@ -46,11 +54,24 @@ const ToolButton: React.FC = ({ id, tool, isSelected, onSelect, }; // Get navigation props for URL support (only if navigation is not disabled) - const navProps = !isUnavailable && !tool.link && !disableNavigation ? getToolNavigation(id, tool) : null; + const navProps = !isDisabled && !tool.link && !disableNavigation ? getToolNavigation(id, tool) : null; - const tooltipContent = isUnavailable - ? (Coming soon: {tool.description}) - : ( + // Determine tooltip content based on disabled reason + let tooltipContent: React.ReactNode; + if (requiresPremiumButNotEnabled) { + tooltipContent = ( + + {t('toolPanel.premiumFeature', 'Premium feature:')} {tool.description} + + ); + } else if (isDisabled) { + tooltipContent = ( + + {t('toolPanel.comingSoon', 'Coming soon:')} {tool.description} + + ); + } else { + tooltipContent = (
{tool.description}
@@ -65,26 +86,39 @@ const ToolButton: React.FC = ({ id, tool, isSelected, onSelect,
); + } const buttonContent = ( <>
- +
+ + {tool.versionStatus === 'alpha' && ( + + {t('toolPanel.alpha', 'Alpha')} + + )} +
{matchedSynonym && ( = ({ id, tool, isSelected, onSelect, > {buttonContent} - ) : tool.link && !isUnavailable ? ( + ) : tool.link && !isDisabled ? ( // For external links, render Button as an anchor with proper href ) : ( - // For unavailable tools, use regular button + // For unavailable/premium tools, use regular button ); - const star = hasStars && !isUnavailable ? ( + const star = hasStars && !isDisabled ? ( toggleFavorite(id as ToolId)} diff --git a/frontend/src/core/contexts/ToolWorkflowContext.tsx b/frontend/src/core/contexts/ToolWorkflowContext.tsx index 8608d460b..5bd7a5da9 100644 --- a/frontend/src/core/contexts/ToolWorkflowContext.tsx +++ b/frontend/src/core/contexts/ToolWorkflowContext.tsx @@ -21,6 +21,7 @@ import { import type { ToolPanelMode } from '@app/constants/toolPanel'; import { usePreferences } from '@app/contexts/PreferencesContext'; import { useToolRegistry } from '@app/contexts/ToolRegistryContext'; +import { useAppConfig } from '@app/contexts/AppConfigContext'; // State interface // Types and reducer/state moved to './toolWorkflow/state' @@ -114,6 +115,8 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { // Tool management hook const { toolRegistry, getSelectedTool } = useToolManagement(); const { allTools } = useToolRegistry(); + const { config } = useAppConfig(); + const premiumEnabled = config?.premiumEnabled; // Tool history hook const { @@ -268,6 +271,13 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { // Workflow actions (compound actions that coordinate multiple state changes) const handleToolSelect = useCallback((toolId: ToolId) => { + // Check if tool requires premium and premium is not enabled + const selectedTool = allTools[toolId]; + if (selectedTool?.requiresPremium === true && premiumEnabled !== true) { + // Premium tool selected without premium - do nothing (should be disabled in UI) + return; + } + // If we're currently on a custom workbench (e.g., Validate Signature report), // selecting any tool should take the user back to the default file manager view. const wasInCustomWorkbench = !isBaseWorkbench(navigationState.workbench); @@ -309,7 +319,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { setSearchQuery(''); setLeftPanelView('toolContent'); setReaderMode(false); // Disable read mode when selecting tools - }, [actions, getSelectedTool, navigationState.workbench, setLeftPanelView, setReaderMode, setSearchQuery]); + }, [actions, getSelectedTool, navigationState.workbench, setLeftPanelView, setReaderMode, setSearchQuery, allTools, premiumEnabled]); const handleBackToTools = useCallback(() => { setLeftPanelView('toolPicker'); diff --git a/frontend/src/core/data/toolsTaxonomy.ts b/frontend/src/core/data/toolsTaxonomy.ts index 0733d9fcc..baebe28cd 100644 --- a/frontend/src/core/data/toolsTaxonomy.ts +++ b/frontend/src/core/data/toolsTaxonomy.ts @@ -59,6 +59,10 @@ export type ToolRegistryEntry = { supportsAutomate?: boolean; // Synonyms for search (optional) synonyms?: string[]; + // Version status indicator (e.g., "alpha", "beta") + versionStatus?: "alpha" | "beta"; + // Whether this tool requires premium access + requiresPremium?: boolean; } export type RegularToolRegistry = Record; diff --git a/frontend/src/core/hooks/useToolManagement.tsx b/frontend/src/core/hooks/useToolManagement.tsx index 3f5676824..81726af91 100644 --- a/frontend/src/core/hooks/useToolManagement.tsx +++ b/frontend/src/core/hooks/useToolManagement.tsx @@ -4,6 +4,7 @@ import { getAllEndpoints, type ToolRegistryEntry, type ToolRegistry } from "@app import { useMultipleEndpointsEnabled } from "@app/hooks/useEndpointConfig"; import { FileId } from '@app/types/file'; import { ToolId } from "@app/types/toolId"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; interface ToolManagementResult { selectedTool: ToolRegistryEntry | null; @@ -15,6 +16,7 @@ interface ToolManagementResult { export const useToolManagement = (): ToolManagementResult => { const [toolSelectedFileIds, setToolSelectedFileIds] = useState([]); + const { config } = useAppConfig(); // Build endpoints list from registry entries with fallback to legacy mapping const { allTools } = useToolRegistry(); @@ -39,11 +41,18 @@ export const useToolManagement = (): ToolManagementResult => { }, [endpointsLoading, endpointStatus, baseRegistry]); const toolRegistry: Partial = useMemo(() => { + // Include tools that either: + // 1. Have enabled endpoints (normal filtering), OR + // 2. Are premium tools (so they show up even if premium is not enabled, but will be disabled) const availableToolRegistry: Partial = {}; (Object.keys(baseRegistry) as ToolId[]).forEach(toolKey => { - if (isToolAvailable(toolKey)) { - const baseTool = baseRegistry[toolKey]; - if (baseTool) { + const baseTool = baseRegistry[toolKey]; + if (baseTool) { + const hasEnabledEndpoints = isToolAvailable(toolKey); + const isPremiumTool = baseTool.requiresPremium === true; + + // Include if endpoints are enabled OR if it's a premium tool (to show it disabled) + if (hasEnabledEndpoints || isPremiumTool) { availableToolRegistry[toolKey] = { ...baseTool, name: baseTool.name, diff --git a/frontend/src/core/hooks/useUrlSync.ts b/frontend/src/core/hooks/useUrlSync.ts index 577dfa5f2..6c9ff903d 100644 --- a/frontend/src/core/hooks/useUrlSync.ts +++ b/frontend/src/core/hooks/useUrlSync.ts @@ -8,6 +8,7 @@ import { parseToolRoute, updateToolRoute, clearToolRoute } from '@app/utils/urlR import { ToolRegistry } from '@app/data/toolsTaxonomy'; import { firePixel } from '@app/utils/scarfTracking'; import { withBasePath } from '@app/constants/app'; +import { useAppConfig } from '@app/contexts/AppConfigContext'; /** * Hook to sync workbench and tool with URL using registry @@ -19,11 +20,33 @@ export function useNavigationUrlSync( registry: ToolRegistry, enableSync: boolean = true ) { + const { config } = useAppConfig(); + const premiumEnabled = config?.premiumEnabled; const hasInitialized = useRef(false); const prevSelectedTool = useRef(null); + + // Check if tool requires premium and redirect if needed + const checkPremiumAndSelect = useCallback((toolId: ToolId) => { + const tool = registry[toolId]; + if (tool?.requiresPremium === true && premiumEnabled !== true) { + // Premium tool accessed without premium - redirect to home + const homePath = withBasePath('/'); + if (window.location.pathname !== homePath) { + clearToolRoute(true); // Use replaceState to avoid adding to history + window.location.href = homePath; + } + return; + } + handleToolSelect(toolId); + }, [registry, premiumEnabled, handleToolSelect]); + // Initialize workbench and tool from URL on mount useEffect(() => { if (!enableSync) return; + // Wait for config to load before checking premium status + if (config === null) return; + // Only run once on initial mount + if (hasInitialized.current) return; // Fire pixel for initial page load const currentPath = window.location.pathname; @@ -32,7 +55,7 @@ export function useNavigationUrlSync( const route = parseToolRoute(registry); if (route.toolId !== selectedTool) { if (route.toolId) { - handleToolSelect(route.toolId); + checkPremiumAndSelect(route.toolId); } else if (selectedTool !== null) { // Only clear selection if we actually had a tool selected // Don't clear on initial load when selectedTool starts as null @@ -41,7 +64,7 @@ export function useNavigationUrlSync( } hasInitialized.current = true; - }, []); // Only run on mount + }, [checkPremiumAndSelect, config, enableSync, registry, selectedTool]); // Include dependencies // Update URL when tool or workbench changes useEffect(() => { @@ -73,7 +96,7 @@ export function useNavigationUrlSync( firePixel(currentPath); if (route.toolId) { - handleToolSelect(route.toolId); + checkPremiumAndSelect(route.toolId); } else { clearToolSelection(); } @@ -82,7 +105,7 @@ export function useNavigationUrlSync( window.addEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState); - }, [selectedTool, handleToolSelect, clearToolSelection, registry, enableSync]); + }, [selectedTool, handleToolSelect, clearToolSelection, registry, enableSync, checkPremiumAndSelect]); } /** diff --git a/frontend/src/core/utils/urlMapping.ts b/frontend/src/core/utils/urlMapping.ts index ba6fe1aaa..6d8b4fde1 100644 --- a/frontend/src/core/utils/urlMapping.ts +++ b/frontend/src/core/utils/urlMapping.ts @@ -126,4 +126,7 @@ export const URL_TO_TOOL_MAP: Record = { '/overlay-pdf': 'overlayPdfs', '/split-pdf-by-sections': 'split', '/split-pdf-by-chapters': 'split', + + // Premium tools + '/pdf-text-editor': 'pdfTextEditor', }; diff --git a/frontend/src/proprietary/data/useProprietaryToolRegistry.tsx b/frontend/src/proprietary/data/useProprietaryToolRegistry.tsx index a5a4dade8..e34ae2ba4 100644 --- a/frontend/src/proprietary/data/useProprietaryToolRegistry.tsx +++ b/frontend/src/proprietary/data/useProprietaryToolRegistry.tsx @@ -20,7 +20,7 @@ export function useProprietaryToolRegistry(): ProprietaryToolRegistry { return useMemo(() => ({ pdfTextEditor: { - icon: , + icon: , name: t("home.pdfTextEditor.title", "PDF Text Editor"), component: PdfTextEditor, description: t( @@ -34,6 +34,8 @@ export function useProprietaryToolRegistry(): ProprietaryToolRegistry { synonyms: getSynonyms(t, "pdfTextEditor"), supportsAutomate: false, automationSettings: null, + versionStatus: "alpha", + requiresPremium: true, }, }), [t]); }