Skip to content
Merged
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
7 changes: 4 additions & 3 deletions frontend/src/core/components/tools/toolPicker/ToolButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { ToolId } from "@app/types/toolId";
import { getToolDisabledReason, getDisabledLabel } from "@app/components/tools/fullscreen/shared";
import { useAppConfig } from "@app/contexts/AppConfigContext";
import { CloudBadge } from "@app/components/shared/CloudBadge";
import { useToolCloudStatus } from "@app/hooks/useToolCloudStatus";
import { useWillUseCloud } from "@app/hooks/useWillUseCloud";

interface ToolButtonProps {
id: ToolId;
Expand All @@ -41,8 +41,9 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
const fav = isFavorite(id as ToolId);

// Check if this tool will route to SaaS backend (desktop only)
const endpointName = tool.endpoints?.[0];
const usesCloud = useToolCloudStatus(endpointName);
const rawEndpoint = tool.operationConfig?.endpoint;
const endpointString = typeof rawEndpoint === 'string' ? rawEndpoint : undefined;
const usesCloud = useWillUseCloud(endpointString);

const handleClick = (id: ToolId) => {
if (isUnavailable) return;
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/core/hooks/tools/convert/useConvertOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@ export const convertOperationConfig = {
customProcessor: convertProcessor, // Can't use callback version here
operationType: 'convert',
defaultParameters,
endpoint: (params: ConvertParameters): string | undefined => {
if (!params.fromExtension || !params.toExtension) return undefined;
const actualToExtension = params.toExtension === 'pdfx' ? 'pdfa' : params.toExtension;
return getEndpointUrl(params.fromExtension, actualToExtension) ?? undefined;
},
} as const;

export const useConvertOperation = (parameters?: ConvertParameters) => {
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/core/hooks/tools/shared/toolOperationTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,13 @@ export interface CustomToolOperationConfig<TParams> extends BaseToolOperationCon
toolType: ToolType.custom;

buildFormData?: undefined;
endpoint?: undefined;

/**
* Optional endpoint for routing decisions (credit check, cloud detection).
* Not used for the API call itself — customProcessor handles that directly.
* Provide a function when the endpoint depends on runtime parameters.
*/
endpoint?: string | ((params: TParams) => string | undefined);

/**
* Custom processing logic that completely bypasses standard file processing.
Expand Down
33 changes: 18 additions & 15 deletions frontend/src/core/hooks/tools/shared/useToolOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,15 @@ export const useToolOperation = <TParams>(
const { processFiles, cancelOperation: cancelApiCalls } = useToolApiCalls<TParams>();
const { generateThumbnails, createDownloadInfo, cleanupBlobUrls, extractZipFiles } = useToolResources();

const { checkCredits } = useCreditCheck(config.operationType);

// Determine endpoint for cloud usage check
const endpointString = config.toolType !== ToolType.custom && config.endpoint
// Determine endpoint for cloud usage check and credit routing.
// For function endpoints, use defaultParameters to get a representative static value.
const endpointString = config.endpoint
? (typeof config.endpoint === 'function'
? (config.defaultParameters ? config.endpoint(config.defaultParameters) : undefined)
? (config.defaultParameters ? (config.endpoint(config.defaultParameters) ?? undefined) : undefined)
: config.endpoint)
: undefined;

const { checkCredits } = useCreditCheck(config.operationType, endpointString);
const willUseCloud = useWillUseCloud(endpointString);

// Track last operation for undo functionality
Expand Down Expand Up @@ -114,22 +115,24 @@ export const useToolOperation = <TParams>(
return;
}

// Get endpoint (static or dynamic) for backend readiness check
const endpoint = config.customProcessor
? undefined // Custom processors may not have endpoints
: typeof config.endpoint === 'function'
? config.endpoint(params)
: config.endpoint;
// Resolve the runtime endpoint from params (static string or function result).
// Custom processors may omit endpoint entirely — result is undefined in that case.
const runtimeEndpoint: string | undefined = config.endpoint
? (typeof config.endpoint === 'function' ? (config.endpoint(params) ?? undefined) : config.endpoint)
: undefined;

// Credit check — no-op in core builds, real check in desktop/SaaS versions
const creditError = await checkCredits();
// Credit check — no-op in core builds, real check in desktop/SaaS versions.
// Pass runtime endpoint so the check can determine if this routes locally (no credits needed).
const creditError = await checkCredits(runtimeEndpoint);
if (creditError !== null) {
actions.setError(creditError);
return;
}

// Backend readiness check (will skip for SaaS-routed endpoints)
const backendReady = await ensureBackendReady(endpoint);
// Backend readiness check (will skip for SaaS-routed endpoints).
// Custom processors without an endpoint skip this — they manage their own backend calls.
const endpointForReadyCheck = config.toolType !== ToolType.custom ? runtimeEndpoint : undefined;
const backendReady = await ensureBackendReady(endpointForReadyCheck);
if (!backendReady) {
actions.setError(t('backendHealth.offline', 'Embedded backend is offline. Please try again shortly.'));
return;
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/core/hooks/useCreditCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
* Desktop layer shadows this with the real implementation
* In web builds, always allows the operation (no credit system)
*/
export function useCreditCheck(_operationType?: string) {
export function useCreditCheck(_operationType?: string, _endpoint?: string) {
return {
checkCredits: async (): Promise<string | null> => null, // null = allowed
checkCredits: async (_runtimeEndpoint?: string): Promise<string | null> => null, // null = allowed
};
}
10 changes: 7 additions & 3 deletions frontend/src/desktop/hooks/useConversionCloudStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,18 @@ export function useConversionCloudStatus(): ConversionStatus {
pairs.map(async ([fromExt, toExt, endpointName]) => {
const key = `${fromExt}-${toExt}`;
try {
const combined = await endpointAvailabilityService.checkEndpointCombined(
// In SaaS mode, everything is available (locally or via cloud routing).
// Only check local support to determine willUseCloud — the same approach
// used by useMultipleEndpointsEnabled's SaaS enhancement.
const availableLocally = await endpointAvailabilityService.isEndpointSupportedLocally(
endpointName,
tauriBackendService.getBackendUrl()
);
return { key, isAvailable: combined.isAvailable, willUseCloud: combined.willUseCloud, localOnly: combined.localOnly };
return { key, isAvailable: true, willUseCloud: !availableLocally, localOnly: false };
} catch (error) {
console.error(`[useConversionCloudStatus] Endpoint check failed for ${key}:`, error);
return { key, isAvailable: false, willUseCloud: false, localOnly: false };
// On error, assume available via cloud (safe default in SaaS mode)
return { key, isAvailable: true, willUseCloud: true, localOnly: false };
}
})
);
Expand Down
19 changes: 16 additions & 3 deletions frontend/src/desktop/hooks/useCreditCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useSaaSBilling } from '@app/contexts/SaasBillingContext';
import { useSaaSMode } from '@app/hooks/useSaaSMode';
import { getToolCreditCost } from '@app/utils/creditCosts';
import { CREDIT_EVENTS } from '@app/constants/creditEvents';
import { operationRouter } from '@app/services/operationRouter';
import type { ToolId } from '@app/types/toolId';

/**
Expand All @@ -15,15 +16,27 @@ import type { ToolId } from '@app/types/toolId';
* Returns null when the operation is allowed, or an error message string
* when it should be blocked.
*/
export function useCreditCheck(operationType?: string) {
export function useCreditCheck(operationType?: string, endpoint?: string) {
const billing = useSaaSBilling();
const isSaaSMode = useSaaSMode();
const { t } = useTranslation();

const checkCredits = useCallback(async (): Promise<string | null> => {
const checkCredits = useCallback(async (runtimeEndpoint?: string): Promise<string | null> => {
if (!isSaaSMode) return null; // Credits only apply in SaaS mode, not self-hosted
if (!billing) return null;

// If the operation routes to the local backend, no credits are consumed — skip check.
// runtimeEndpoint (from params at execution time) takes priority over hook-level endpoint.
const ep = runtimeEndpoint ?? endpoint;
if (ep) {
try {
const willUseSaaS = await operationRouter.willRouteToSaaS(ep);
if (!willUseSaaS) return null;
} catch {
// If routing check fails, fall through to credit check as a safe default.
}
}

const { creditBalance, loading } = billing;
const requiredCredits = getToolCreditCost(operationType as ToolId);

Expand All @@ -44,7 +57,7 @@ export function useCreditCheck(operationType?: string) {
}

return null;
}, [billing, isSaaSMode, operationType, t]);
}, [billing, isSaaSMode, operationType, endpoint, t]);

return { checkCredits };
}
4 changes: 2 additions & 2 deletions frontend/src/saas/hooks/useCreditCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { getToolCreditCost } from '@app/utils/creditCosts';
import { openPlanSettings } from '@app/utils/appSettings';
import type { ToolId } from '@app/types/toolId';

export function useCreditCheck(operationType?: string) {
export function useCreditCheck(operationType?: string, _endpoint?: string) {
const { hasSufficientCredits, isPro, creditBalance, refreshCredits } = useCredits();
const { t } = useTranslation();

const checkCredits = useCallback(async (): Promise<string | null> => {
const checkCredits = useCallback(async (_runtimeEndpoint?: string): Promise<string | null> => {
const requiredCredits = getToolCreditCost(operationType as ToolId);
const creditCheck = hasSufficientCredits(requiredCredits);

Expand Down
Loading