WIP: AI Podcast Maker and YouTube Creator Studio integration

This commit is contained in:
ajaysi
2025-12-10 09:37:55 +05:30
parent 31f078c763
commit 81590cf4db
75 changed files with 11879 additions and 1380 deletions

View File

@@ -0,0 +1,71 @@
import { useState, useCallback } from 'react';
interface BudgetTrackingState {
totalSpent: number;
budgetCap: number;
operations: Array<{ id: string; cost: number; timestamp: string; description: string }>;
}
export const useBudgetTracking = (initialBudgetCap: number = 50) => {
const [budget, setBudget] = useState<BudgetTrackingState>({
totalSpent: 0,
budgetCap: initialBudgetCap,
operations: [],
});
const addCost = useCallback((cost: number, description: string) => {
setBudget((prev) => {
const newTotal = prev.totalSpent + cost;
const operation = {
id: `${Date.now()}_${Math.random()}`,
cost,
timestamp: new Date().toISOString(),
description,
};
return {
...prev,
totalSpent: newTotal,
operations: [...prev.operations, operation],
};
});
}, []);
const setBudgetCap = useCallback((cap: number) => {
setBudget((prev) => ({ ...prev, budgetCap: cap }));
}, []);
const reset = useCallback(() => {
setBudget({
totalSpent: 0,
budgetCap: initialBudgetCap,
operations: [],
});
}, [initialBudgetCap]);
const canAfford = useCallback((estimatedCost: number): boolean => {
return budget.totalSpent + estimatedCost <= budget.budgetCap;
}, [budget.totalSpent, budget.budgetCap]);
const getRemaining = useCallback((): number => {
return Math.max(0, budget.budgetCap - budget.totalSpent);
}, [budget.budgetCap, budget.totalSpent]);
const getUsagePercentage = useCallback((): number => {
if (budget.budgetCap === 0) return 0;
return Math.min(100, (budget.totalSpent / budget.budgetCap) * 100);
}, [budget.totalSpent, budget.budgetCap]);
return {
totalSpent: budget.totalSpent,
budgetCap: budget.budgetCap,
remaining: getRemaining(),
usagePercentage: getUsagePercentage(),
operations: budget.operations,
addCost,
setBudgetCap,
canAfford,
reset,
};
};

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { useAuth } from '@clerk/clerk-react';
export interface ContentAsset {
@@ -49,40 +49,100 @@ const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || 'http://localhost:800
export const useContentAssets = (filters: AssetFilters = {}) => {
const { getToken } = useAuth();
const [assets, setAssets] = useState<ContentAsset[]>([]);
const [loading, setLoading] = useState(true);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [total, setTotal] = useState(0);
const isFetchingRef = useRef(false);
const abortControllerRef = useRef<AbortController | null>(null);
// Memoize filters to create stable reference - only changes when actual values change
const stableFilters = useMemo(() => {
return {
asset_type: filters.asset_type,
source_module: filters.source_module,
search: filters.search,
tags: filters.tags,
favorites_only: filters.favorites_only,
limit: filters.limit,
offset: filters.offset,
};
}, [
filters.asset_type,
filters.source_module,
filters.search,
filters.tags?.join(','),
filters.favorites_only,
filters.limit,
filters.offset,
]);
// Create stable filter key for comparison
const filterKey = useMemo(() => {
return JSON.stringify(stableFilters);
}, [stableFilters]);
// Store latest filters in ref for use in fetch function
const filtersRef = useRef(stableFilters);
useEffect(() => {
filtersRef.current = stableFilters;
}, [stableFilters]);
// Fetch function - exposed for manual retry, not called automatically on errors
const fetchAssets = useCallback(async () => {
// Prevent concurrent fetches
if (isFetchingRef.current) {
return;
}
// Cancel any pending request
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// Create new abort controller for this request
const abortController = new AbortController();
abortControllerRef.current = abortController;
try {
isFetchingRef.current = true;
setLoading(true);
setError(null);
const token = await getToken();
if (!token) {
throw new Error('Not authenticated');
setLoading(false);
isFetchingRef.current = false;
return;
}
// Use ref to get latest filters
const currentFilters = filtersRef.current;
const params = new URLSearchParams();
if (filters.asset_type) params.append('asset_type', filters.asset_type);
if (filters.source_module) params.append('source_module', filters.source_module);
if (filters.search) params.append('search', filters.search);
if (filters.tags && filters.tags.length > 0) params.append('tags', filters.tags.join(','));
if (filters.favorites_only) params.append('favorites_only', 'true');
params.append('limit', String(filters.limit || 100));
params.append('offset', String(filters.offset || 0));
// Add cache busting for fresh data
params.append('_t', String(Date.now()));
if (currentFilters.asset_type) params.append('asset_type', currentFilters.asset_type);
if (currentFilters.source_module) params.append('source_module', currentFilters.source_module);
if (currentFilters.search) params.append('search', currentFilters.search);
if (currentFilters.tags && currentFilters.tags.length > 0) params.append('tags', currentFilters.tags.join(','));
if (currentFilters.favorites_only) params.append('favorites_only', 'true');
params.append('limit', String(currentFilters.limit || 100));
params.append('offset', String(currentFilters.offset || 0));
const response = await fetch(`${API_BASE_URL}/api/content-assets/?${params.toString()}`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
signal: abortController.signal,
});
if (!response.ok) {
if (response.status === 429) {
setError('Rate limit exceeded. Please try again later.');
setAssets([]);
setTotal(0);
setLoading(false);
isFetchingRef.current = false;
return;
}
throw new Error(`Failed to fetch assets: ${response.statusText}`);
}
@@ -90,16 +150,34 @@ export const useContentAssets = (filters: AssetFilters = {}) => {
setAssets(data.assets);
setTotal(data.total);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch assets');
// Don't set error for aborted requests
if (err instanceof Error && err.name === 'AbortError') {
return;
}
if (err instanceof TypeError && err.message.includes('fetch')) {
setError('Network error. Please check your connection.');
} else {
setError(err instanceof Error ? err.message : 'Failed to fetch assets');
}
setAssets([]);
} finally {
setLoading(false);
isFetchingRef.current = false;
}
}, [getToken, filters]);
}, [getToken]); // Only depend on getToken, use ref for filters
// Fetch on mount and when filters change - but only once per filter change
// NO automatic retry on errors - user must call refetch() manually
useEffect(() => {
fetchAssets();
}, [fetchAssets]);
// Cleanup: abort on unmount or filter change
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, [filterKey, fetchAssets]); // Include fetchAssets but it's stable due to ref usage
const toggleFavorite = useCallback(async (assetId: number) => {
try {

View File

@@ -0,0 +1,372 @@
import { useState, useCallback, useEffect, useRef } from 'react';
import {
PodcastAnalysis,
PodcastEstimate,
Query,
Research,
Script,
Knobs,
Job,
CreateProjectPayload,
} from '../components/PodcastMaker/types';
import { BlogResearchResponse, ResearchProvider } from '../services/blogWriterApi';
import { podcastApi } from '../services/podcastApi';
export interface PodcastProjectState {
// Project metadata
project: { id: string; idea: string; duration: number; speakers: number } | null;
// Step results
analysis: PodcastAnalysis | null;
queries: Query[];
selectedQueries: Set<string>;
research: Research | null;
rawResearch: BlogResearchResponse | null;
estimate: PodcastEstimate | null;
scriptData: Script | null;
// Render jobs
renderJobs: Job[];
// Settings
knobs: Knobs;
researchProvider: ResearchProvider;
budgetCap: number;
// UI state
showScriptEditor: boolean;
showRenderQueue: boolean;
// Current step tracking
currentStep: 'create' | 'analysis' | 'research' | 'script' | 'render' | null;
// Timestamps
createdAt?: string;
updatedAt?: string;
}
const DEFAULT_KNOBS: Knobs = {
voice_emotion: "neutral",
voice_speed: 1,
resolution: "720p",
scene_length_target: 45,
sample_rate: 24000,
bitrate: "standard",
};
const DEFAULT_STATE: PodcastProjectState = {
project: null,
analysis: null,
queries: [],
selectedQueries: new Set(),
research: null,
rawResearch: null,
estimate: null,
scriptData: null,
renderJobs: [],
knobs: DEFAULT_KNOBS,
researchProvider: "google",
budgetCap: 50,
showScriptEditor: false,
showRenderQueue: false,
currentStep: null,
};
const STORAGE_KEY = 'podcast_project_state';
export const usePodcastProjectState = () => {
const [state, setState] = useState<PodcastProjectState>(() => {
// Initialize from localStorage if available
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
const parsed = JSON.parse(saved);
// Restore Sets from arrays
const restoredState: PodcastProjectState = {
...DEFAULT_STATE,
...parsed,
selectedQueries: parsed.selectedQueries ? new Set(parsed.selectedQueries) : new Set(),
renderJobs: parsed.renderJobs || [],
};
return restoredState;
}
} catch (error) {
console.error('Error loading podcast project state from localStorage:', error);
}
return DEFAULT_STATE;
});
// Debounce ref for database sync
const syncTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Persist state to localStorage on every change
useEffect(() => {
try {
// Convert Sets to arrays for JSON serialization
const serializableState = {
...state,
selectedQueries: Array.from(state.selectedQueries),
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(serializableState));
} catch (error) {
console.error('Error saving podcast project state to localStorage:', error);
}
}, [state]);
// Sync to database after major steps (debounced)
useEffect(() => {
if (!state.project || !state.project.id) return;
// Capture project ID to avoid closure issues
const projectId = state.project.id;
// Clear existing timeout
if (syncTimeoutRef.current) {
clearTimeout(syncTimeoutRef.current);
}
// Debounce database sync (wait 2 seconds after last change)
syncTimeoutRef.current = setTimeout(async () => {
try {
const dbState = {
analysis: state.analysis,
queries: state.queries,
selected_queries: Array.from(state.selectedQueries),
research: state.research,
raw_research: state.rawResearch,
estimate: state.estimate,
script_data: state.scriptData,
render_jobs: state.renderJobs,
knobs: state.knobs,
research_provider: state.researchProvider,
show_script_editor: state.showScriptEditor,
show_render_queue: state.showRenderQueue,
current_step: state.currentStep,
status: state.currentStep === 'render' && state.renderJobs.every(j => j.status === 'completed') ? 'completed' : 'in_progress',
};
await podcastApi.saveProject(projectId, dbState);
} catch (error) {
console.error('Error syncing project to database:', error);
// Don't throw - localStorage is still working
}
}, 2000);
return () => {
if (syncTimeoutRef.current) {
clearTimeout(syncTimeoutRef.current);
}
};
}, [
state.project,
state.analysis,
state.queries,
state.selectedQueries,
state.research,
state.rawResearch,
state.estimate,
state.scriptData,
state.renderJobs,
state.knobs,
state.researchProvider,
state.showScriptEditor,
state.showRenderQueue,
state.currentStep,
]);
// Setters
const setProject = useCallback((project: PodcastProjectState['project']) => {
setState((prev) => ({ ...prev, project, currentStep: project ? 'analysis' : null, updatedAt: new Date().toISOString() }));
}, []);
const setAnalysis = useCallback((analysis: PodcastProjectState['analysis']) => {
setState((prev) => ({
...prev,
analysis,
currentStep: analysis ? 'research' : prev.currentStep,
updatedAt: new Date().toISOString()
}));
}, []);
const setQueries = useCallback((queries: Query[]) => {
setState((prev) => ({ ...prev, queries, updatedAt: new Date().toISOString() }));
}, []);
const setSelectedQueries = useCallback((selectedQueries: Set<string> | ((prev: Set<string>) => Set<string>)) => {
setState((prev) => {
const newQueries = typeof selectedQueries === 'function' ? selectedQueries(prev.selectedQueries) : selectedQueries;
return { ...prev, selectedQueries: newQueries, updatedAt: new Date().toISOString() };
});
}, []);
const setResearch = useCallback((research: PodcastProjectState['research']) => {
setState((prev) => ({
...prev,
research,
currentStep: research ? 'script' : prev.currentStep,
updatedAt: new Date().toISOString()
}));
}, []);
const setRawResearch = useCallback((rawResearch: PodcastProjectState['rawResearch']) => {
setState((prev) => ({ ...prev, rawResearch, updatedAt: new Date().toISOString() }));
}, []);
const setEstimate = useCallback((estimate: PodcastProjectState['estimate']) => {
setState((prev) => ({ ...prev, estimate, updatedAt: new Date().toISOString() }));
}, []);
const setScriptData = useCallback((scriptData: PodcastProjectState['scriptData']) => {
setState((prev) => ({
...prev,
scriptData,
currentStep: scriptData ? 'render' : prev.currentStep,
updatedAt: new Date().toISOString()
}));
}, []);
const setRenderJobs = useCallback((renderJobs: Job[]) => {
setState((prev) => ({ ...prev, renderJobs, updatedAt: new Date().toISOString() }));
}, []);
const updateRenderJob = useCallback((sceneId: string, updates: Partial<Job>) => {
setState((prev) => ({
...prev,
renderJobs: prev.renderJobs.map((job) =>
job.sceneId === sceneId ? { ...job, ...updates } : job
),
updatedAt: new Date().toISOString(),
}));
}, []);
const setKnobs = useCallback((knobs: Knobs) => {
setState((prev) => ({ ...prev, knobs, updatedAt: new Date().toISOString() }));
}, []);
const setResearchProvider = useCallback((provider: ResearchProvider) => {
setState((prev) => ({ ...prev, researchProvider: provider, updatedAt: new Date().toISOString() }));
}, []);
const setBudgetCap = useCallback((cap: number) => {
setState((prev) => ({ ...prev, budgetCap: cap, updatedAt: new Date().toISOString() }));
}, []);
const setShowScriptEditor = useCallback((show: boolean) => {
setState((prev) => ({ ...prev, showScriptEditor: show, updatedAt: new Date().toISOString() }));
}, []);
const setShowRenderQueue = useCallback((show: boolean) => {
setState((prev) => ({ ...prev, showRenderQueue: show, updatedAt: new Date().toISOString() }));
}, []);
const setCurrentStep = useCallback((step: PodcastProjectState['currentStep']) => {
setState((prev) => ({ ...prev, currentStep: step, updatedAt: new Date().toISOString() }));
}, []);
// Reset state
const resetState = useCallback(() => {
setState(DEFAULT_STATE);
localStorage.removeItem(STORAGE_KEY);
}, []);
// Initialize project from payload
const initializeProject = useCallback(async (payload: CreateProjectPayload, projectId: string) => {
// Create project in database
try {
await podcastApi.createProjectInDb({
project_id: projectId,
idea: payload.ideaOrUrl,
duration: payload.duration,
speakers: payload.speakers,
budget_cap: payload.budgetCap,
});
} catch (error) {
console.error('Error creating project in database:', error);
// Continue anyway - localStorage fallback
}
setState((prev) => ({
...prev,
project: {
id: projectId,
idea: payload.ideaOrUrl,
duration: payload.duration,
speakers: payload.speakers,
},
knobs: payload.knobs,
budgetCap: payload.budgetCap,
currentStep: 'analysis',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}));
}, []);
// Load project from database
const loadProjectFromDb = useCallback(async (projectId: string) => {
try {
const dbProject = await podcastApi.loadProject(projectId);
// Restore state from database
setState((prev) => ({
...prev,
project: {
id: dbProject.project_id,
idea: dbProject.idea,
duration: dbProject.duration,
speakers: dbProject.speakers,
},
analysis: dbProject.analysis,
queries: dbProject.queries || [],
selectedQueries: new Set(dbProject.selected_queries || []),
research: dbProject.research,
rawResearch: dbProject.raw_research,
estimate: dbProject.estimate,
scriptData: dbProject.script_data,
renderJobs: dbProject.render_jobs || [],
knobs: dbProject.knobs || DEFAULT_KNOBS,
researchProvider: dbProject.research_provider || 'google',
budgetCap: dbProject.budget_cap || 50,
showScriptEditor: dbProject.show_script_editor || false,
showRenderQueue: dbProject.show_render_queue || false,
currentStep: dbProject.current_step || null,
createdAt: dbProject.created_at,
updatedAt: dbProject.updated_at,
}));
} catch (error) {
console.error('Error loading project from database:', error);
throw error;
}
}, []);
return {
// State
...state,
// Setters
setProject,
setAnalysis,
setQueries,
setSelectedQueries,
setResearch,
setRawResearch,
setEstimate,
setScriptData,
setRenderJobs,
updateRenderJob,
setKnobs,
setResearchProvider,
setBudgetCap,
setShowScriptEditor,
setShowRenderQueue,
setCurrentStep,
// Helpers
resetState,
initializeProject,
loadProjectFromDb,
};
};

View File

@@ -1,257 +1,82 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import {
checkPreflight,
PreflightOperation,
PreflightCheckResponse,
PreflightLimitInfo,
} from '../services/billingService';
import { useState, useCallback } from 'react';
import { checkPreflight, PreflightOperation, PreflightCheckResponse } from '../services/billingService';
export interface UsePreflightCheckOptions {
operation: PreflightOperation;
enabled?: boolean; // Whether to perform check on hover
debounceMs?: number; // Debounce delay (default: 300ms)
cacheTtl?: number; // Cache TTL in ms (default: 5000ms)
onBlocked?: (response: PreflightCheckResponse) => void;
onAllowed?: (response: PreflightCheckResponse) => void;
}
export interface UsePreflightCheckResult {
canProceed: boolean;
estimatedCost: number;
limitInfo: PreflightLimitInfo | null;
loading: boolean;
error: string | null;
checkOnHover: () => void;
checkNow: () => void; // Immediate check
reset: () => void;
}
interface CacheEntry {
data: PreflightCheckResponse;
timestamp: number;
}
/**
* React hook for pre-flight checking operations with cost estimation.
*
* Features:
* - Debounced hover checks (300ms default)
* - In-memory caching (5s default TTL)
* - Request cancellation on unmount
*/
export const usePreflightCheck = (
options: UsePreflightCheckOptions
): UsePreflightCheckResult => {
const {
operation,
enabled = true,
debounceMs = 300,
cacheTtl = 5000,
} = options;
const [canProceed, setCanProceed] = useState<boolean>(true);
const [estimatedCost, setEstimatedCost] = useState<number>(0);
const [limitInfo, setLimitInfo] = useState<PreflightLimitInfo | null>(null);
const [loading, setLoading] = useState<boolean>(false);
export const usePreflightCheck = (options?: UsePreflightCheckOptions) => {
const [isChecking, setIsChecking] = useState(false);
const [lastCheck, setLastCheck] = useState<PreflightCheckResponse | null>(null);
const [error, setError] = useState<string | null>(null);
// Cache for pre-flight check results
const cacheRef = useRef<Map<string, CacheEntry>>(new Map());
// Debounce timer ref
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
// Abort controller for request cancellation
const abortControllerRef = useRef<AbortController | null>(null);
// Generate cache key from operation
const getCacheKey = useCallback(() => {
return JSON.stringify(operation);
}, [operation]);
// Check if cached result is still valid
const getCachedResult = useCallback((): PreflightCheckResponse | null => {
const cacheKey = getCacheKey();
const cached = cacheRef.current.get(cacheKey);
if (cached) {
const age = Date.now() - cached.timestamp;
if (age < cacheTtl) {
return cached.data;
}
// Cache expired, remove it
cacheRef.current.delete(cacheKey);
}
return null;
}, [getCacheKey, cacheTtl]);
// Store result in cache
const setCache = useCallback((data: PreflightCheckResponse) => {
const cacheKey = getCacheKey();
cacheRef.current.set(cacheKey, {
data,
timestamp: Date.now(),
});
}, [getCacheKey]);
// Perform actual pre-flight check
const performCheck = useCallback(async (): Promise<void> => {
if (!enabled) {
return;
}
// Check cache first
const cached = getCachedResult();
if (cached) {
updateState(cached);
return;
}
// Cancel any in-flight request
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// Create new abort controller
abortControllerRef.current = new AbortController();
const currentAbortController = abortControllerRef.current;
setLoading(true);
const check = useCallback(async (operation: PreflightOperation): Promise<PreflightCheckResponse> => {
setIsChecking(true);
setError(null);
try {
const response = await checkPreflight(operation);
setLastCheck(response);
// Check if request was cancelled
if (currentAbortController.signal.aborted) {
return;
}
// Cache the result
setCache(response);
// Update state
updateState(response);
} catch (err: any) {
// Check if request was cancelled
if (currentAbortController.signal.aborted) {
return;
}
const errorMessage = err?.message || 'Pre-flight check failed';
setError(errorMessage);
setCanProceed(false);
setEstimatedCost(0);
setLimitInfo(null);
} finally {
if (!currentAbortController.signal.aborted) {
setLoading(false);
}
}
}, [operation, enabled, getCachedResult, setCache]);
// Update state from response
const updateState = useCallback((response: PreflightCheckResponse) => {
setCanProceed(response.can_proceed);
setEstimatedCost(response.estimated_cost);
// Get limit info from first operation (for single operation checks)
const firstOp = response.operations[0];
if (firstOp) {
setLimitInfo(firstOp.limit_info);
if (!response.can_proceed && firstOp.message) {
setError(firstOp.message);
if (!response.can_proceed) {
setError(response.operations[0]?.message || 'Operation blocked by subscription limits');
options?.onBlocked?.(response);
} else {
setError(null);
options?.onAllowed?.(response);
}
} else {
setLimitInfo(null);
return response;
} catch (err: any) {
const errorMessage = err?.response?.data?.detail || err?.message || 'Preflight check failed';
setError(errorMessage);
// Return blocked response on error
const blockedResponse: PreflightCheckResponse = {
can_proceed: false,
estimated_cost: 0,
operations: [{
provider: operation.provider,
operation_type: operation.operation_type,
cost: 0,
allowed: false,
limit_info: null,
message: errorMessage,
}],
total_cost: 0,
usage_summary: null,
cached: false,
};
setLastCheck(blockedResponse);
options?.onBlocked?.(blockedResponse);
return blockedResponse;
} finally {
setIsChecking(false);
}
}, []);
}, [options]);
// Debounced check for hover events
const checkOnHover = useCallback(() => {
if (!enabled) {
return;
}
// Clear existing timer
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
// Check cache first (no debounce for cache hits)
const cached = getCachedResult();
if (cached) {
updateState(cached);
return;
}
// Debounce the actual API call
debounceTimerRef.current = setTimeout(() => {
performCheck();
}, debounceMs);
}, [enabled, debounceMs, getCachedResult, updateState, performCheck]);
// Immediate check (no debounce)
const checkNow = useCallback(() => {
if (!enabled) {
return;
}
// Clear any pending debounced check
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = null;
}
performCheck();
}, [enabled, performCheck]);
// Reset state
const reset = useCallback(() => {
setCanProceed(true);
setEstimatedCost(0);
setLimitInfo(null);
setLoading(false);
setError(null);
// Clear debounce timer
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = null;
}
// Cancel any in-flight request
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
}, []);
// Cleanup on unmount
useEffect(() => {
return () => {
// Clear debounce timer
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
// Cancel any in-flight request
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
// Extract useful properties from lastCheck
const estimatedCost = lastCheck?.estimated_cost ?? 0;
const limitInfo = lastCheck?.operations?.[0]?.limit_info ?? null;
return {
canProceed,
check,
isChecking,
lastCheck,
error,
canProceed: lastCheck?.can_proceed ?? null,
estimatedCost,
limitInfo,
loading,
error,
checkOnHover,
checkNow,
reset,
loading: isChecking,
// For backward compatibility with OperationButton
checkOnHover: () => {}, // No-op for now, can be implemented if needed
checkNow: () => check(lastCheck?.operations?.[0] ? {
provider: lastCheck.operations[0].provider,
operation_type: lastCheck.operations[0].operation_type,
} as PreflightOperation : {
provider: 'gemini',
operation_type: 'unknown',
}),
};
};