+ {isBrowserOnly && (
+
+
+
+ The Agent Inbox requires the Friday desktop app (Tauri). In browser mode, use the{" "}
+ Notifications panel (bell icon in the sidebar) to see agent activity from the Python backend.
+
+
+ )}
{isLoading ? (
Loading agent inbox...
) : (
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index 55817d6..bcaac29 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -9,8 +9,7 @@ import AnalyticsProvider from '@/components/AnalyticsProvider'
import { Toaster, toast } from 'sonner'
import "sonner/dist/styles.css"
import { useState, useEffect, useCallback } from 'react'
-import { listen, UnlistenFn } from '@tauri-apps/api/event'
-import { invoke } from '@tauri-apps/api/core'
+import { safeInvoke, safeListen, isTauriAvailable } from '@/lib/tauri-compat'
import { TooltipProvider } from '@/components/ui/tooltip'
import { RecordingStateProvider } from '@/contexts/RecordingStateContext'
import { NotificationsProvider } from '@/contexts/NotificationsContext'
@@ -79,7 +78,13 @@ export default function RootLayout({
useEffect(() => {
// Check onboarding status first
- invoke<{ completed: boolean } | null>('get_onboarding_status')
+ if (!isTauriAvailable()) {
+ console.log('[Layout] Tauri not available — skipping onboarding for browser dev')
+ setShowOnboarding(false)
+ setOnboardingCompleted(true)
+ return
+ }
+ safeInvoke<{ completed: boolean } | null>('get_onboarding_status')
.then((status) => {
const isComplete = status?.completed ?? false
setOnboardingCompleted(isComplete)
@@ -93,7 +98,6 @@ export default function RootLayout({
})
.catch((error) => {
console.error('[Layout] Failed to check onboarding status:', error)
- // Default to showing onboarding if we can't check
setShowOnboarding(true)
setOnboardingCompleted(false)
})
@@ -109,7 +113,7 @@ export default function RootLayout({
}, []);
useEffect(() => {
// Listen for tray recording toggle request
- const unlisten = listen('request-recording-toggle', () => {
+ const unlistenPromise = safeListen('request-recording-toggle', () => {
console.log('[Layout] Received request-recording-toggle from tray');
if (showOnboarding) {
@@ -124,7 +128,7 @@ export default function RootLayout({
});
return () => {
- unlisten.then(fn => fn());
+ unlistenPromise.then(fn => fn());
};
}, [showOnboarding]);
@@ -159,14 +163,14 @@ export default function RootLayout({
// Listen for drag-drop events
useEffect(() => {
- if (showOnboarding) return; // Don't handle drops during onboarding
+ if (showOnboarding || !isTauriAvailable()) return; // Don't handle drops during onboarding or browser mode
- const unlisteners: UnlistenFn[] = [];
+ const unlisteners: (() => void)[] = [];
const cleanedUpRef = { current: false };
const setupListeners = async () => {
// Drag enter/over - show overlay only if beta feature is enabled
- const unlistenDragEnter = await listen('tauri://drag-enter', () => {
+ const unlistenDragEnter = await safeListen('tauri://drag-enter', () => {
if (loadBetaFeatures().importAndRetranscribe) {
setShowDropOverlay(true);
}
@@ -178,7 +182,7 @@ export default function RootLayout({
unlisteners.push(unlistenDragEnter);
// Drag leave - hide overlay
- const unlistenDragLeave = await listen('tauri://drag-leave', () => {
+ const unlistenDragLeave = await safeListen('tauri://drag-leave', () => {
setShowDropOverlay(false);
});
if (cleanedUpRef.current) {
@@ -189,7 +193,7 @@ export default function RootLayout({
unlisteners.push(unlistenDragLeave);
// Drop - process files
- const unlistenDrop = await listen<{ paths: string[] }>('tauri://drag-drop', (event) => {
+ const unlistenDrop = await safeListen<{ paths: string[] }>('tauri://drag-drop', (event) => {
setShowDropOverlay(false);
handleFileDrop(event.payload.paths);
});
diff --git a/frontend/src/components/AnalyticsProvider.tsx b/frontend/src/components/AnalyticsProvider.tsx
index dc1b75f..a1f7455 100644
--- a/frontend/src/components/AnalyticsProvider.tsx
+++ b/frontend/src/components/AnalyticsProvider.tsx
@@ -2,7 +2,7 @@
import React, { useEffect, ReactNode, useRef, useState, createContext } from 'react';
import Analytics from '@/lib/analytics';
-import { load } from '@tauri-apps/plugin-store';
+import { isTauriAvailable } from '@/lib/tauri-compat';
interface AnalyticsProviderProps {
@@ -30,6 +30,13 @@ export default function AnalyticsProvider({ children }: AnalyticsProviderProps)
}
const initAnalytics = async () => {
+ if (!isTauriAvailable()) {
+ // In browser mode, skip analytics entirely
+ console.log('[Analytics] Tauri not available — skipping analytics init');
+ initialized.current = true;
+ return;
+ }
+ const { load } = await import('@tauri-apps/plugin-store');
const store = await load('analytics.json', {
autoSave: false,
defaults: {
@@ -63,6 +70,7 @@ export default function AnalyticsProvider({ children }: AnalyticsProviderProps)
const deviceInfo = await Analytics.getDeviceInfo();
// Store platform info in analytics.json for quick access
+ const { load } = await import('@tauri-apps/plugin-store');
const store = await load('analytics.json', {
autoSave: false,
defaults: {
diff --git a/frontend/src/components/Sidebar/SidebarProvider.tsx b/frontend/src/components/Sidebar/SidebarProvider.tsx
index 243ed21..40142fb 100644
--- a/frontend/src/components/Sidebar/SidebarProvider.tsx
+++ b/frontend/src/components/Sidebar/SidebarProvider.tsx
@@ -3,7 +3,7 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import Analytics from '@/lib/analytics';
-import { invoke } from '@tauri-apps/api/core';
+import { safeInvoke } from '@/lib/tauri-compat';
import { useRecordingState } from '@/contexts/RecordingStateContext';
@@ -86,7 +86,7 @@ export function SidebarProvider({ children }: { children: React.ReactNode }) {
const fetchMeetings = React.useCallback(async () => {
if (serverAddress) {
try {
- const meetings = await invoke('api_get_meetings') as Array<{ id: string, title: string }>;
+ const meetings = await safeInvoke('api_get_meetings') as Array<{ id: string, title: string }>;
const transformedMeetings = meetings.map((meeting: any) => ({
id: meeting.id,
title: meeting.title
@@ -174,7 +174,7 @@ export function SidebarProvider({ children }: { children: React.ReactNode }) {
setIsSearching(true);
- const results = await invoke('api_search_transcripts', { query }) as TranscriptSearchResult[];
+ const results = await safeInvoke('api_search_transcripts', { query }) as TranscriptSearchResult[];
setSearchResults(results);
} catch (error) {
console.error('Error searching transcripts:', error);
@@ -219,7 +219,7 @@ export function SidebarProvider({ children }: { children: React.ReactNode }) {
return;
}
try {
- const result = await invoke('api_get_summary', {
+ const result = await safeInvoke('api_get_summary', {
meetingId: meetingId,
}) as any;
diff --git a/frontend/src/components/Sidebar/index.tsx b/frontend/src/components/Sidebar/index.tsx
index a6074a9..6fdd305 100644
--- a/frontend/src/components/Sidebar/index.tsx
+++ b/frontend/src/components/Sidebar/index.tsx
@@ -10,7 +10,7 @@ import { ModelConfig } from '@/components/ModelSettingsModal';
import { SettingTabs } from '../SettingTabs';
import { TranscriptModelProps } from '@/components/TranscriptSettings';
import Analytics from '@/lib/analytics';
-import { invoke } from '@tauri-apps/api/core';
+import { safeInvoke, safeListen, isTauriAvailable } from '@/lib/tauri-compat';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { toast } from 'sonner';
import { useRecordingState } from '@/contexts/RecordingStateContext';
@@ -118,12 +118,12 @@ const Sidebar: React.FC = () => {
}
try {
- const data = await invoke('api_get_model_config') as any;
+ const data = await safeInvoke('api_get_model_config') as any;
if (data && data.provider !== null) {
// Fetch API key if not included and provider requires it
if (data.provider !== 'ollama' && !data.apiKey) {
try {
- const apiKeyData = await invoke('api_get_api_key', {
+ const apiKeyData = await safeInvoke('api_get_api_key', {
provider: data.provider
}) as string;
data.apiKey = apiKeyData;
@@ -152,7 +152,7 @@ const Sidebar: React.FC = () => {
}
try {
- const data = await invoke('api_get_transcript_config') as any;
+ const data = await safeInvoke('api_get_transcript_config') as any;
if (data && data.provider !== null) {
setTranscriptModelConfig(data);
}
@@ -166,8 +166,7 @@ const Sidebar: React.FC = () => {
// Listen for model config updates from other components
useEffect(() => {
const setupListener = async () => {
- const { listen } = await import('@tauri-apps/api/event');
- const unlisten = await listen
('model-config-updated', (event) => {
+ const unlisten = await safeListen('model-config-updated', (event) => {
console.log('Sidebar received model-config-updated event:', event.payload);
setModelConfig(event.payload);
});
@@ -188,7 +187,7 @@ const Sidebar: React.FC = () => {
// Handle model config save
const handleSaveModelConfig = async (config: ModelConfig) => {
try {
- await invoke('api_save_model_config', {
+ await safeInvoke('api_save_model_config', {
provider: config.provider,
model: config.model,
whisperModel: config.whisperModel,
@@ -201,8 +200,10 @@ const Sidebar: React.FC = () => {
setSettingsSaveSuccess(true);
// Emit event to sync other components
- const { emit } = await import('@tauri-apps/api/event');
- await emit('model-config-updated', config);
+ if (isTauriAvailable()) {
+ const { emit } = await import('@tauri-apps/api/event');
+ await emit('model-config-updated', config);
+ }
// Track settings change
await Analytics.trackSettingsChanged('model_config', `${config.provider}_${config.model}`);
@@ -222,7 +223,7 @@ const Sidebar: React.FC = () => {
};
console.log('Saving transcript config with payload:', payload);
- await invoke('api_save_transcript_config', {
+ await safeInvoke('api_save_transcript_config', {
provider: payload.provider,
model: payload.model,
apiKey: payload.apiKey,
@@ -328,8 +329,7 @@ const Sidebar: React.FC = () => {
};
try {
- const { invoke } = await import('@tauri-apps/api/core');
- await invoke('api_delete_meeting', {
+ await safeInvoke('api_delete_meeting', {
meetingId: itemId,
});
console.log('Meeting deleted successfully');
@@ -387,7 +387,7 @@ const Sidebar: React.FC = () => {
}
try {
- await invoke('api_save_meeting_title', {
+ await safeInvoke('api_save_meeting_title', {
meetingId: meetingId,
title: newTitle,
});
diff --git a/frontend/src/contexts/ConfigContext.tsx b/frontend/src/contexts/ConfigContext.tsx
index 6aa6011..e38139e 100644
--- a/frontend/src/contexts/ConfigContext.tsx
+++ b/frontend/src/contexts/ConfigContext.tsx
@@ -4,7 +4,7 @@ import React, { createContext, useContext, useState, useEffect, useCallback, use
import { TranscriptModelProps } from '@/components/TranscriptSettings';
import { SelectedDevices } from '@/components/DeviceSelection';
import { configService, ModelConfig } from '@/services/configService';
-import { invoke } from '@tauri-apps/api/core';
+import { safeInvoke, safeListen, isTauriAvailable } from '@/lib/tauri-compat';
import Analytics from '@/lib/analytics';
import { BetaFeatures, BetaFeatureKey, loadBetaFeatures, saveBetaFeatures } from '@/types/betaFeatures';
@@ -180,7 +180,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
const loadModels = async () => {
try {
const endpoint = modelConfig.ollamaEndpoint || null;
- const modelList = await invoke('get_ollama_models', { endpoint });
+ const modelList = await safeInvoke('get_ollama_models', { endpoint });
setModels(modelList);
setError('');
} catch (err) {
@@ -214,7 +214,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
// Sync language preference to Rust on mount (fixes startup desync bug)
useEffect(() => {
if (selectedLanguage) {
- invoke('set_language_preference', { language: selectedLanguage })
+ safeInvoke('set_language_preference', { language: selectedLanguage })
.then(() => {
console.log('[ConfigContext] Synced language preference to Rust on startup:', selectedLanguage);
})
@@ -298,7 +298,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
const providers = ['claude', 'groq', 'openai', 'openrouter'];
const keys = await Promise.all(
providers.map(p =>
- invoke('api_get_api_key', { provider: p })
+ safeInvoke('api_get_api_key', { provider: p })
.catch(() => null) // Gracefully handle missing keys
)
);
@@ -321,8 +321,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
// Listen for model config updates from other components
useEffect(() => {
const setupListener = async () => {
- const { listen } = await import('@tauri-apps/api/event');
- const unlisten = await listen('model-config-updated', (event) => {
+ const unlisten = await safeListen('model-config-updated', (event) => {
console.log('[ConfigContext] Received model-config-updated event:', event.payload);
setModelConfig(event.payload);
@@ -429,7 +428,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
// Load notification settings from backend
let settings: NotificationSettings | null = null;
try {
- settings = await invoke('get_notification_settings');
+ settings = await safeInvoke('get_notification_settings');
setNotificationSettings(settings);
} catch (notifError) {
console.error('[ConfigContext] Failed to load notification settings:', notifError);
@@ -439,9 +438,9 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
// Load storage locations
const [dbDir, modelsDir, recordingsDir] = await Promise.all([
- invoke('get_database_directory'),
- invoke('whisper_get_models_directory'),
- invoke('get_default_recordings_folder_path')
+ safeInvoke('get_database_directory'),
+ safeInvoke('whisper_get_models_directory'),
+ safeInvoke('get_default_recordings_folder_path')
]);
setStorageLocations({
@@ -463,7 +462,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
// Update notification settings
const updateNotificationSettings = useCallback(async (settings: NotificationSettings) => {
try {
- await invoke('set_notification_settings', { settings });
+ await safeInvoke('set_notification_settings', { settings });
setNotificationSettings(settings);
} catch (error) {
console.error('[ConfigContext] Failed to update notification settings:', error);
@@ -478,7 +477,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
localStorage.setItem('primaryLanguage', lang);
}
// Sync with Rust in-memory state for live recording
- invoke('set_language_preference', { language: lang }).catch(err =>
+ safeInvoke('set_language_preference', { language: lang }).catch(err =>
console.error('Failed to sync language preference to Rust:', err)
);
}, []);
diff --git a/frontend/src/contexts/OllamaDownloadContext.tsx b/frontend/src/contexts/OllamaDownloadContext.tsx
index a5e6270..a82d251 100644
--- a/frontend/src/contexts/OllamaDownloadContext.tsx
+++ b/frontend/src/contexts/OllamaDownloadContext.tsx
@@ -1,7 +1,7 @@
'use client';
import React, { createContext, useContext, useState, useEffect } from 'react';
-import { listen } from '@tauri-apps/api/event';
+import { safeListen } from '@/lib/tauri-compat';
import { toast } from 'sonner';
/**
@@ -48,7 +48,7 @@ export function OllamaDownloadProvider({ children }: { children: React.ReactNode
const setupListeners = async () => {
try {
// Download progress
- const unlistenProgress = await listen<{ modelName: string; progress: number }>(
+ const unlistenProgress = await safeListen<{ modelName: string; progress: number }>(
'ollama-model-download-progress',
(event) => {
const { modelName, progress } = event.payload;
@@ -72,7 +72,7 @@ export function OllamaDownloadProvider({ children }: { children: React.ReactNode
unsubscribers.push(unlistenProgress);
// Download complete
- const unlistenComplete = await listen<{ modelName: string }>(
+ const unlistenComplete = await safeListen<{ modelName: string }>(
'ollama-model-download-complete',
(event) => {
const { modelName } = event.payload;
@@ -100,7 +100,7 @@ export function OllamaDownloadProvider({ children }: { children: React.ReactNode
unsubscribers.push(unlistenComplete);
// Download error
- const unlistenError = await listen<{ modelName: string; error: string }>(
+ const unlistenError = await safeListen<{ modelName: string; error: string }>(
'ollama-model-download-error',
(event) => {
const { modelName, error } = event.payload;
diff --git a/frontend/src/contexts/OnboardingContext.tsx b/frontend/src/contexts/OnboardingContext.tsx
index 27dda96..cbf86aa 100644
--- a/frontend/src/contexts/OnboardingContext.tsx
+++ b/frontend/src/contexts/OnboardingContext.tsx
@@ -1,8 +1,7 @@
'use client';
import React, { createContext, useContext, useState, useEffect, useRef, useCallback } from 'react';
-import { invoke } from '@tauri-apps/api/core';
-import { listen } from '@tauri-apps/api/event';
+import { safeInvoke, safeListen, isTauriAvailable } from '@/lib/tauri-compat';
import type { PermissionStatus, OnboardingPermissions } from '@/types/onboarding';
const PARAKEET_MODEL = 'parakeet-tdt-0.6b-v3-int8';
@@ -103,6 +102,7 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
// Load status on mount and initialize database
useEffect(() => {
+ if (!isTauriAvailable()) return; // Skip all Tauri-only initialization in browser mode
loadOnboardingStatus();
checkDatabaseStatus();
initializeDatabaseInBackground();
@@ -110,7 +110,7 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
// Fetch and set recommended model
const fetchRecommendation = async () => {
try {
- const recommendedModel = await invoke('builtin_ai_get_recommended_model');
+ const recommendedModel = await safeInvoke('builtin_ai_get_recommended_model');
setSelectedSummaryModel(recommendedModel);
console.log('[OnboardingContext] Set recommended model:', recommendedModel);
} catch (error) {
@@ -125,7 +125,7 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
const initializeDatabaseInBackground = async () => {
try {
console.log('[OnboardingContext] Starting background database initialization');
- const isFirstLaunch = await invoke('check_first_launch');
+ const isFirstLaunch = await safeInvoke('check_first_launch');
if (!isFirstLaunch) {
console.log('[OnboardingContext] Database exists, skipping initialization');
@@ -146,14 +146,14 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
if (typeof navigator !== 'undefined' && navigator.platform?.toLowerCase().includes('mac')) {
const homebrewDbPath = '/usr/local/var/friday/meeting_minutes.db';
try {
- const homebrewCheck = await invoke<{ exists: boolean; size: number } | null>(
+ const homebrewCheck = await safeInvoke<{ exists: boolean; size: number } | null>(
'check_homebrew_database',
{ path: homebrewDbPath }
);
if (homebrewCheck?.exists) {
console.log('[OnboardingContext] Found Homebrew database, importing');
- await invoke('import_and_initialize_database', { legacyDbPath: homebrewDbPath });
+ await safeInvoke('import_and_initialize_database', { legacyDbPath: homebrewDbPath });
setDatabaseExists(true);
return;
}
@@ -164,10 +164,10 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
// Check default legacy database location
try {
- const legacyPath = await invoke('check_default_legacy_database');
+ const legacyPath = await safeInvoke('check_default_legacy_database');
if (legacyPath) {
console.log('[OnboardingContext] Found legacy database, importing');
- await invoke('import_and_initialize_database', { legacyDbPath: legacyPath });
+ await safeInvoke('import_and_initialize_database', { legacyDbPath: legacyPath });
setDatabaseExists(true);
return;
}
@@ -177,7 +177,7 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
// No legacy database found - initialize fresh
console.log('[OnboardingContext] No legacy database found, initializing fresh');
- await invoke('initialize_fresh_database');
+ await safeInvoke('initialize_fresh_database');
setDatabaseExists(true);
};
@@ -202,7 +202,7 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
// Listen to Parakeet download progress
useEffect(() => {
- const unlisten = listen<{
+ const unlisten = safeListen<{
modelName: string;
progress: number;
downloaded_mb?: number;
@@ -228,7 +228,7 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
}
);
- const unlistenComplete = listen<{ modelName: string }>(
+ const unlistenComplete = safeListen<{ modelName: string }>(
'parakeet-model-download-complete',
(event) => {
const { modelName } = event.payload;
@@ -239,7 +239,7 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
}
);
- const unlistenError = listen<{ modelName: string; error: string }>(
+ const unlistenError = safeListen<{ modelName: string; error: string }>(
'parakeet-model-download-error',
(event) => {
const { modelName } = event.payload;
@@ -258,7 +258,7 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
// Listen to summary model (Built-in AI) download progress
useEffect(() => {
- const unlisten = listen<{
+ const unlisten = safeListen<{
model: string;
progress: number;
downloaded_mb?: number;
@@ -292,7 +292,7 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
const checkDatabaseStatus = async () => {
try {
- const isFirstLaunch = await invoke('check_first_launch');
+ const isFirstLaunch = await safeInvoke('check_first_launch');
setDatabaseExists(!isFirstLaunch);
console.log('[OnboardingContext] Database exists:', !isFirstLaunch);
} catch (error) {
@@ -303,7 +303,7 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
const loadOnboardingStatus = async () => {
try {
- const status = await invoke('get_onboarding_status');
+ const status = await safeInvoke('get_onboarding_status');
if (status) {
console.log('[OnboardingContext] Loaded saved status:', status);
@@ -332,8 +332,8 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
// Verify Parakeet model exists on disk
try {
- await invoke('parakeet_init');
- parakeetDownloaded = await invoke('parakeet_has_available_models');
+ await safeInvoke('parakeet_init');
+ parakeetDownloaded = await safeInvoke('parakeet_has_available_models');
console.log('[OnboardingContext] Parakeet verified on disk:', parakeetDownloaded);
} catch (error) {
console.warn('[OnboardingContext] Failed to verify Parakeet:', error);
@@ -343,7 +343,7 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
// Verify Summary model exists on disk - check if ANY model is available
// Onboarding always uses builtin-ai (local models)
try {
- const availableModel = await invoke('builtin_ai_get_available_summary_model');
+ const availableModel = await safeInvoke('builtin_ai_get_available_summary_model');
summaryModelDownloaded = !!availableModel;
console.log('[OnboardingContext] Summary model verified on disk:', summaryModelDownloaded, 'model:', availableModel);
} catch (error) {
@@ -381,7 +381,7 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
}
try {
- await invoke('save_onboarding_status_cmd', {
+ await safeInvoke('save_onboarding_status_cmd', {
status: {
version: '1.0',
completed: completed,
@@ -410,7 +410,7 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
}
// Onboarding always uses builtin-ai with selected model
- await invoke('complete_onboarding', {
+ await safeInvoke('complete_onboarding', {
model: selectedSummaryModel,
});
setCompleted(true);
@@ -433,7 +433,7 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
saveTimeoutRef.current = undefined;
}
- await invoke('complete_onboarding_hosted', { apiKey });
+ await safeInvoke('complete_onboarding_hosted', { apiKey });
setCompleted(true);
console.log('[OnboardingContext] Onboarding completed in hosted (Gemini) mode');
isCompletingRef.current = false;
@@ -453,7 +453,7 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
// Start Parakeet download first (speech recognition - always required)
if (!parakeetDownloaded) {
console.log('[OnboardingContext] Starting Parakeet download');
- invoke('parakeet_download_model', { modelName: PARAKEET_MODEL })
+ safeInvoke('parakeet_download_model', { modelName: PARAKEET_MODEL })
.catch(err => console.error('[OnboardingContext] Parakeet download failed:', err));
}
@@ -461,7 +461,7 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
if (includeGemma && !summaryModelDownloaded) {
setTimeout(() => {
console.log('[OnboardingContext] Starting Gemma download (delayed to prioritize Parakeet)');
- invoke('builtin_ai_download_model', { modelName: selectedSummaryModel || 'gemma3:1b' })
+ safeInvoke('builtin_ai_download_model', { modelName: selectedSummaryModel || 'gemma3:1b' })
.catch(err => console.error('[OnboardingContext] Gemma download failed:', err));
}, 3000); // 3 second delay to give Parakeet priority
}
@@ -475,7 +475,7 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
// Check if any models are currently downloading (for re-entry)
const checkActiveDownloads = async () => {
try {
- const models = await invoke('parakeet_get_available_models');
+ const models = await safeInvoke('parakeet_get_available_models');
const isDownloading = models.some(m => m.status && (typeof m.status === 'object' ? 'Downloading' in m.status : m.status === 'Downloading'));
if (isDownloading) {
@@ -493,7 +493,7 @@ export function OnboardingProvider({ children }: { children: React.ReactNode })
const retryParakeetDownload = async () => {
console.log('[OnboardingContext] Retrying Parakeet download');
try {
- await invoke('parakeet_retry_download', { modelName: PARAKEET_MODEL });
+ await safeInvoke('parakeet_retry_download', { modelName: PARAKEET_MODEL });
} catch (error) {
console.error('[OnboardingContext] Retry failed:', error);
throw error;
diff --git a/frontend/src/contexts/RecordingPostProcessingProvider.tsx b/frontend/src/contexts/RecordingPostProcessingProvider.tsx
index e298970..c9870fa 100644
--- a/frontend/src/contexts/RecordingPostProcessingProvider.tsx
+++ b/frontend/src/contexts/RecordingPostProcessingProvider.tsx
@@ -1,7 +1,7 @@
'use client';
import React, { useEffect } from 'react';
-import { listen } from '@tauri-apps/api/event';
+import { safeListen } from '@/lib/tauri-compat';
import { useRecordingStop } from '@/hooks/useRecordingStop';
/**
@@ -33,7 +33,7 @@ export function RecordingPostProcessingProvider({ children }: { children: React.
const setupListener = async () => {
try {
// Listen for recording-stop-complete event from Rust
- unlistenFn = await listen('recording-stop-complete', (event) => {
+ unlistenFn = await safeListen('recording-stop-complete', (event) => {
console.log('[RecordingPostProcessing] Received recording-stop-complete event:', event.payload);
// Call the post-processing handler
diff --git a/frontend/src/lib/analytics.ts b/frontend/src/lib/analytics.ts
index 24ec285..f44c8c8 100644
--- a/frontend/src/lib/analytics.ts
+++ b/frontend/src/lib/analytics.ts
@@ -1,4 +1,10 @@
-import { invoke } from '@tauri-apps/api/core';
+import { isTauriAvailable } from '@/lib/tauri-compat';
+
+const tauriInvoke = async (cmd: string, args?: Record): Promise => {
+ if (!isTauriAvailable()) throw new Error('Tauri not available');
+ const { invoke } = await import('@tauri-apps/api/core');
+ return invoke(cmd, args);
+};
export interface AnalyticsProperties {
[key: string]: string;
@@ -43,7 +49,7 @@ export class Analytics {
private static async doInit(): Promise {
try {
- await invoke('init_analytics');
+ await tauriInvoke('init_analytics');
this.initialized = true;
console.log('Analytics initialized successfully');
} catch (error) {
@@ -56,7 +62,7 @@ export class Analytics {
static async disable(): Promise {
try {
- await invoke('disable_analytics');
+ await tauriInvoke('disable_analytics');
this.initialized = false;
this.currentUserId = null;
this.initializationPromise = null;
@@ -68,7 +74,7 @@ export class Analytics {
static async isEnabled(): Promise {
try {
- return await invoke('is_analytics_enabled');
+ return await tauriInvoke('is_analytics_enabled');
} catch (error) {
console.error('Failed to check analytics status:', error);
return false;
@@ -82,7 +88,7 @@ export class Analytics {
}
try {
- await invoke('track_event', { eventName, properties });
+ await tauriInvoke('track_event', { eventName, properties });
} catch (error) {
console.error(`Failed to track event ${eventName}:`, error);
}
@@ -95,7 +101,7 @@ export class Analytics {
}
try {
- await invoke('identify_user', { userId, properties });
+ await tauriInvoke('identify_user', { userId, properties });
this.currentUserId = userId;
} catch (error) {
console.error(`Failed to identify user ${userId}:`, error);
@@ -110,7 +116,7 @@ export class Analytics {
}
try {
- const sessionId = await invoke('start_analytics_session', { userId });
+ const sessionId = await tauriInvoke('start_analytics_session', { userId });
this.currentUserId = userId;
return sessionId as string;
@@ -124,7 +130,7 @@ export class Analytics {
if (!this.initialized) return;
try {
- await invoke('end_analytics_session');
+ await tauriInvoke('end_analytics_session');
} catch (error) {
console.error('Failed to end analytics session:', error);
}
@@ -134,7 +140,7 @@ export class Analytics {
if (!this.initialized) return;
try {
- await invoke('track_daily_active_user');
+ await tauriInvoke('track_daily_active_user');
} catch (error) {
console.error('Failed to track daily active user:', error);
}
@@ -144,7 +150,7 @@ export class Analytics {
if (!this.initialized) return;
try {
- await invoke('track_user_first_launch');
+ await tauriInvoke('track_user_first_launch');
} catch (error) {
console.error('Failed to track user first launch:', error);
}
@@ -154,7 +160,7 @@ export class Analytics {
if (!this.initialized) return false;
try {
- return await invoke('is_analytics_session_active');
+ return await tauriInvoke('is_analytics_session_active');
} catch (error) {
console.error('Failed to check session status:', error);
return false;
@@ -535,7 +541,7 @@ export class Analytics {
if (!this.initialized) return;
try {
- await invoke('track_meeting_started', { meetingId, meetingTitle });
+ await tauriInvoke('track_meeting_started', { meetingId, meetingTitle });
} catch (error) {
console.error('Failed to track meeting started:', error);
}
@@ -545,7 +551,7 @@ export class Analytics {
if (!this.initialized) return;
try {
- await invoke('track_recording_started', { meetingId });
+ await tauriInvoke('track_recording_started', { meetingId });
} catch (error) {
console.error('Failed to track recording started:', error);
}
@@ -555,7 +561,7 @@ export class Analytics {
if (!this.initialized) return;
try {
- await invoke('track_recording_stopped', { meetingId, durationSeconds });
+ await tauriInvoke('track_recording_stopped', { meetingId, durationSeconds });
} catch (error) {
console.error('Failed to track recording stopped:', error);
}
@@ -565,7 +571,7 @@ export class Analytics {
if (!this.initialized) return;
try {
- await invoke('track_meeting_deleted', { meetingId });
+ await tauriInvoke('track_meeting_deleted', { meetingId });
} catch (error) {
console.error('Failed to track meeting deleted:', error);
}
@@ -575,7 +581,7 @@ export class Analytics {
if (!this.initialized) return;
try {
- await invoke('track_settings_changed', { settingType, newValue });
+ await tauriInvoke('track_settings_changed', { settingType, newValue });
} catch (error) {
console.error('Failed to track settings changed:', error);
}
@@ -585,7 +591,7 @@ export class Analytics {
if (!this.initialized) return;
try {
- await invoke('track_feature_used', { featureName });
+ await tauriInvoke('track_feature_used', { featureName });
} catch (error) {
console.error('Failed to track feature used:', error);
}
@@ -652,7 +658,7 @@ export class Analytics {
try {
console.log('Tracking backend connection event:', { success, error });
- await invoke('track_event', {
+ await tauriInvoke('track_event', {
eventName: 'backend_connection',
properties: {
success: success.toString(),
@@ -675,7 +681,7 @@ export class Analytics {
try {
console.log('Tracking transcription error event:', { errorMessage });
- await invoke('track_event', {
+ await tauriInvoke('track_event', {
eventName: 'transcription_error',
properties: {
error_message: errorMessage,
@@ -697,7 +703,7 @@ export class Analytics {
try {
console.log('Tracking transcription success event:', { duration });
- await invoke('track_event', {
+ await tauriInvoke('track_event', {
eventName: 'transcription_success',
properties: {
duration: duration ? duration.toString() : '',
@@ -764,7 +770,7 @@ export class Analytics {
try {
console.log('Tracking summary generation completed event:', { modelProvider, modelName, success, durationSeconds, errorMessage });
- await invoke('track_summary_generation_completed', {
+ await tauriInvoke('track_summary_generation_completed', {
modelProvider,
modelName,
success,
@@ -785,7 +791,7 @@ export class Analytics {
try {
console.log('Tracking summary regenerated event:', { modelProvider, modelName });
- await invoke('track_summary_regenerated', {
+ await tauriInvoke('track_summary_regenerated', {
modelProvider,
modelName
});
@@ -803,7 +809,7 @@ export class Analytics {
try {
console.log('Tracking model changed event:', { oldProvider, oldModel, newProvider, newModel });
- await invoke('track_model_changed', {
+ await tauriInvoke('track_model_changed', {
oldProvider,
oldModel,
newProvider,
@@ -823,7 +829,7 @@ export class Analytics {
try {
console.log('Tracking custom prompt used event:', { promptLength });
- await invoke('track_custom_prompt_used', {
+ await tauriInvoke('track_custom_prompt_used', {
promptLength
});
console.log('Custom prompt used event tracked successfully');
diff --git a/frontend/src/lib/tauri-compat.ts b/frontend/src/lib/tauri-compat.ts
new file mode 100644
index 0000000..091a470
--- /dev/null
+++ b/frontend/src/lib/tauri-compat.ts
@@ -0,0 +1,145 @@
+/**
+ * Tauri compatibility layer for browser-only dev mode.
+ *
+ * When Tauri is available, delegates to `invoke()`.
+ * When running in a plain browser, maps known commands to HTTP calls
+ * against the Python backend at http://localhost:5167.
+ */
+
+const BACKEND = "http://localhost:5167";
+
+export function isTauriAvailable(): boolean {
+ return (
+ typeof window !== "undefined" && !!(window as any).__TAURI_INTERNALS__
+ );
+}
+
+// Maps Tauri command names → { method, path, bodyMapper? }
+// bodyMapper transforms invoke args into a fetch body (POST only).
+const COMMAND_MAP: Record<
+ string,
+ {
+ method: "GET" | "POST";
+ path: (args?: any) => string;
+ bodyMapper?: (args?: any) => any;
+ }
+> = {
+ api_get_meetings: {
+ method: "GET",
+ path: () => "/get-meetings",
+ },
+ api_search_transcripts: {
+ method: "POST",
+ path: () => "/search-transcripts",
+ bodyMapper: (args) => ({ query: args?.query }),
+ },
+ api_get_summary: {
+ method: "GET",
+ path: (args) => `/get-summary/${args?.meetingId}`,
+ },
+ api_get_model_config: {
+ method: "GET",
+ path: () => "/get-model-config",
+ },
+ api_get_transcript_config: {
+ method: "GET",
+ path: () => "/get-transcript-config",
+ },
+ api_get_api_key: {
+ method: "POST",
+ path: () => "/get-api-key",
+ bodyMapper: (args) => ({ provider: args?.provider }),
+ },
+ api_save_model_config: {
+ method: "POST",
+ path: () => "/save-model-config",
+ bodyMapper: (args) => ({
+ provider: args?.provider,
+ model: args?.model,
+ whisper_model: args?.whisperModel,
+ api_key: args?.apiKey,
+ ollama_endpoint: args?.ollamaEndpoint,
+ }),
+ },
+ api_save_transcript_config: {
+ method: "POST",
+ path: () => "/save-transcript-config",
+ bodyMapper: (args) => ({
+ provider: args?.provider,
+ model: args?.model,
+ api_key: args?.apiKey,
+ }),
+ },
+ api_delete_meeting: {
+ method: "POST",
+ path: () => "/delete-meeting",
+ bodyMapper: (args) => ({ meeting_id: args?.meetingId }),
+ },
+ api_save_meeting_title: {
+ method: "POST",
+ path: () => "/save-meeting-title",
+ bodyMapper: (args) => ({
+ meeting_id: args?.meetingId,
+ title: args?.title,
+ }),
+ },
+ api_get_meeting: {
+ method: "GET",
+ path: (args) => `/get-meeting/${args?.meetingId}`,
+ },
+};
+
+async function httpFallback(cmd: string, args?: Record): Promise {
+ const mapping = COMMAND_MAP[cmd];
+ if (!mapping) {
+ throw new Error(
+ `Command "${cmd}" is not available in browser mode (requires Tauri desktop app).`
+ );
+ }
+
+ const url = `${BACKEND}${mapping.path(args)}`;
+ const init: RequestInit = {
+ method: mapping.method,
+ headers: { "Content-Type": "application/json" },
+ };
+ if (mapping.method === "POST" && mapping.bodyMapper) {
+ init.body = JSON.stringify(mapping.bodyMapper(args));
+ }
+
+ const res = await fetch(url, init);
+ if (!res.ok) {
+ const text = await res.text().catch(() => res.statusText);
+ throw new Error(`${cmd} failed: ${res.status} ${text}`);
+ }
+ return res.json() as Promise;
+}
+
+/**
+ * Drop-in replacement for Tauri `invoke()`.
+ * Uses Tauri IPC when available, falls back to HTTP in browser mode.
+ */
+export async function safeInvoke(
+ cmd: string,
+ args?: Record
+): Promise {
+ if (isTauriAvailable()) {
+ const { invoke } = await import("@tauri-apps/api/core");
+ return invoke(cmd, args);
+ }
+ return httpFallback(cmd, args);
+}
+
+/**
+ * Safe wrapper for Tauri `listen()`. Returns a no-op unlisten in browser mode.
+ */
+export async function safeListen(
+ event: string,
+ handler: (event: { payload: T }) => void
+): Promise<() => void> {
+ if (isTauriAvailable()) {
+ const { listen } = await import("@tauri-apps/api/event");
+ return listen(event, handler);
+ }
+ // No-op in browser mode
+ return () => {};
+}
diff --git a/frontend/src/services/agentService.ts b/frontend/src/services/agentService.ts
index 28e1e20..8f035fd 100644
--- a/frontend/src/services/agentService.ts
+++ b/frontend/src/services/agentService.ts
@@ -1,5 +1,3 @@
-import { invoke } from '@tauri-apps/api/core';
-
export interface AgentSettingsPayload {
enabled: boolean;
provider: string;
@@ -85,57 +83,99 @@ export interface AgentRecommendationActionResponse {
created_calendar_event: CreatedCalendarEventSummary | null;
}
+const isTauri = (): boolean =>
+ typeof window !== "undefined" && !!(window as any).__TAURI_INTERNALS__;
+
+async function tauriInvoke(cmd: string, args?: Record): Promise {
+ if (!isTauri()) {
+ throw new Error("This feature requires the Friday desktop app (Tauri runtime not available).");
+ }
+ const { invoke } = await import("@tauri-apps/api/core");
+ return invoke(cmd, args);
+}
+
+const DEFAULT_SETTINGS: AgentSettingsPayload = {
+ enabled: false,
+ provider: "gemini",
+ model: "gemini-2.0-flash",
+ notifications_enabled: true,
+ calendar_proposals_enabled: false,
+ heartbeat_interval_minutes: 10,
+};
+
+const DEFAULT_STATUS: AgentStatusResponse = {
+ settings: DEFAULT_SETTINGS,
+ api_key_configured: false,
+ calendar_connected: false,
+ calendar_can_write: false,
+ is_running: false,
+ last_run_at: null,
+ last_success_at: null,
+ last_error: null,
+ pending_recommendations: 0,
+ open_tasks: 0,
+};
+
class AgentService {
+ get available(): boolean {
+ return isTauri();
+ }
+
async getStatus(): Promise {
- return invoke('agent_get_status');
+ if (!isTauri()) return DEFAULT_STATUS;
+ return tauriInvoke('agent_get_status');
}
async getSettings(): Promise {
- return invoke('agent_get_settings');
+ if (!isTauri()) return DEFAULT_SETTINGS;
+ return tauriInvoke('agent_get_settings');
}
async setSettings(settings: AgentSettingsPayload): Promise {
- return invoke('agent_set_settings', { settings });
+ return tauriInvoke('agent_set_settings', { settings });
}
async saveGeminiApiKey(apiKey: string): Promise {
- return invoke('agent_save_gemini_api_key', { apiKey });
+ return tauriInvoke('agent_save_gemini_api_key', { apiKey });
}
async clearGeminiApiKey(): Promise {
- return invoke('agent_clear_gemini_api_key');
+ return tauriInvoke('agent_clear_gemini_api_key');
}
async runHeartbeatNow(): Promise {
- return invoke('agent_run_heartbeat_now');
+ return tauriInvoke('agent_run_heartbeat_now');
}
async listRecommendations(status?: string): Promise {
- return invoke('agent_list_recommendations', { status: status ?? null });
+ if (!isTauri()) return [];
+ return tauriInvoke('agent_list_recommendations', { status: status ?? null });
}
async acceptRecommendation(recommendationId: string): Promise {
- return invoke('agent_accept_recommendation', { recommendationId });
+ return tauriInvoke('agent_accept_recommendation', { recommendationId });
}
async dismissRecommendation(recommendationId: string): Promise {
- return invoke('agent_dismiss_recommendation', { recommendationId });
+ return tauriInvoke('agent_dismiss_recommendation', { recommendationId });
}
async listMemory(limit = 25): Promise {
- return invoke('agent_list_memory', { limit });
+ if (!isTauri()) return [];
+ return tauriInvoke('agent_list_memory', { limit });
}
async getMeetingContext(meetingId: string): Promise {
- return invoke('agent_get_meeting_context', { meetingId });
+ return tauriInvoke('agent_get_meeting_context', { meetingId });
}
async listTasks(status?: string): Promise {
- return invoke('agent_list_tasks', { status: status ?? null });
+ if (!isTauri()) return [];
+ return tauriInvoke('agent_list_tasks', { status: status ?? null });
}
async updateTaskStatus(taskId: string, status: string): Promise {
- return invoke('agent_update_task_status', { taskId, status });
+ return tauriInvoke('agent_update_task_status', { taskId, status });
}
}
diff --git a/frontend/src/services/calendarService.ts b/frontend/src/services/calendarService.ts
index ab33947..5e91294 100644
--- a/frontend/src/services/calendarService.ts
+++ b/frontend/src/services/calendarService.ts
@@ -1,4 +1,13 @@
-import { invoke } from '@tauri-apps/api/core';
+const isTauri = (): boolean =>
+ typeof window !== "undefined" && !!(window as any).__TAURI_INTERNALS__;
+
+async function tauriInvoke(cmd: string, args?: Record): Promise {
+ if (!isTauri()) {
+ throw new Error("Google Calendar requires the Friday desktop app (Tauri runtime not available).");
+ }
+ const { invoke } = await import("@tauri-apps/api/core");
+ return invoke(cmd, args);
+}
export interface CalendarAccountSummary {
email: string | null;
@@ -71,41 +80,51 @@ export interface UpcomingCalendarEvent {
html_link: string | null;
}
+const DEFAULT_STATUS: CalendarStatusResponse = {
+ client_configured: false,
+ connected: false,
+ can_write: false,
+ syncing: false,
+ account: null,
+};
+
class CalendarService {
async getStatus(): Promise {
- return invoke('calendar_get_status');
+ if (!isTauri()) return DEFAULT_STATUS;
+ return tauriInvoke('calendar_get_status');
}
async listUpcoming(): Promise {
- return invoke('calendar_list_upcoming');
+ if (!isTauri()) return [];
+ return tauriInvoke('calendar_list_upcoming');
}
async connectGoogle(writeAccess = false): Promise {
- return invoke('calendar_connect_google', {
+ return tauriInvoke('calendar_connect_google', {
writeAccess,
});
}
async upgradeGoogleAccess(): Promise {
- return invoke('calendar_upgrade_google_access');
+ return tauriInvoke('calendar_upgrade_google_access');
}
async disconnectGoogle(): Promise {
- return invoke('calendar_disconnect_google');
+ return tauriInvoke('calendar_disconnect_google');
}
async syncNow(): Promise {
- return invoke('calendar_sync_now');
+ return tauriInvoke('calendar_sync_now');
}
async getMeetingLink(meetingId: string): Promise {
- return invoke('calendar_get_meeting_link', {
+ return tauriInvoke('calendar_get_meeting_link', {
meetingId,
});
}
async getLinkCandidates(meetingId: string): Promise {
- return invoke('calendar_get_link_candidates', {
+ return tauriInvoke('calendar_get_link_candidates', {
meetingId,
});
}
@@ -114,14 +133,14 @@ class CalendarService {
meetingId: string,
providerEventId: string
): Promise {
- return invoke('calendar_set_meeting_link', {
+ return tauriInvoke('calendar_set_meeting_link', {
meetingId,
providerEventId,
});
}
async clearMeetingLink(meetingId: string): Promise {
- return invoke('calendar_clear_meeting_link', { meetingId });
+ return tauriInvoke('calendar_clear_meeting_link', { meetingId });
}
}
diff --git a/frontend/src/services/configService.ts b/frontend/src/services/configService.ts
index b554e4a..5915a3b 100644
--- a/frontend/src/services/configService.ts
+++ b/frontend/src/services/configService.ts
@@ -5,7 +5,7 @@
* Pure 1-to-1 wrapper - no error handling changes, exact same behavior as direct invoke calls.
*/
-import { invoke } from '@tauri-apps/api/core';
+import { safeInvoke } from '@/lib/tauri-compat';
import { TranscriptModelProps } from '@/components/TranscriptSettings';
export interface ModelConfig {
@@ -51,7 +51,7 @@ export class ConfigService {
* @returns Promise with { provider, model, apiKey }
*/
async getTranscriptConfig(): Promise {
- return invoke('api_get_transcript_config');
+ return safeInvoke('api_get_transcript_config');
}
/**
@@ -59,7 +59,7 @@ export class ConfigService {
* @returns Promise with { provider, model, whisperModel }
*/
async getModelConfig(): Promise {
- return invoke('api_get_model_config');
+ return safeInvoke('api_get_model_config');
}
/**
@@ -67,7 +67,7 @@ export class ConfigService {
* @returns Promise with { preferred_mic_device, preferred_system_device }
*/
async getRecordingPreferences(): Promise {
- return invoke('get_recording_preferences');
+ return safeInvoke('get_recording_preferences');
}
/**
@@ -75,7 +75,7 @@ export class ConfigService {
* @returns Promise with CustomOpenAIConfig or null if not configured
*/
async getCustomOpenAIConfig(): Promise {
- return invoke('api_get_custom_openai_config');
+ return safeInvoke('api_get_custom_openai_config');
}
/**
@@ -84,7 +84,7 @@ export class ConfigService {
* @returns Promise with result status
*/
async saveCustomOpenAIConfig(config: CustomOpenAIConfig): Promise<{ status: string; message: string }> {
- return invoke<{ status: string; message: string }>('api_save_custom_openai_config', {
+ return safeInvoke<{ status: string; message: string }>('api_save_custom_openai_config', {
endpoint: config.endpoint,
apiKey: config.apiKey,
model: config.model,
@@ -106,7 +106,7 @@ export class ConfigService {
apiKey: string | null,
model: string
): Promise<{ status: string; message: string; http_status?: number }> {
- return invoke<{ status: string; message: string; http_status?: number }>('api_test_custom_openai_connection', {
+ return safeInvoke<{ status: string; message: string; http_status?: number }>('api_test_custom_openai_connection', {
endpoint,
apiKey,
model,
diff --git a/frontend/src/services/recordingService.ts b/frontend/src/services/recordingService.ts
index 4ffb9d2..34e5263 100644
--- a/frontend/src/services/recordingService.ts
+++ b/frontend/src/services/recordingService.ts
@@ -5,8 +5,9 @@
* Pure 1-to-1 wrapper - no error handling changes, exact same behavior as direct invoke/listen calls.
*/
-import { invoke } from '@tauri-apps/api/core';
-import { listen, UnlistenFn } from '@tauri-apps/api/event';
+import { safeInvoke, safeListen } from '@/lib/tauri-compat';
+
+type UnlistenFn = () => void;
export interface RecordingState {
is_recording: boolean;
@@ -32,7 +33,7 @@ export class RecordingService {
* @returns Promise
*/
async isRecording(): Promise {
- return invoke('is_recording');
+ return safeInvoke('is_recording');
}
/**
@@ -40,7 +41,7 @@ export class RecordingService {
* @returns Promise with full recording state
*/
async getRecordingState(): Promise {
- return invoke('get_recording_state');
+ return safeInvoke('get_recording_state');
}
/**
@@ -48,7 +49,7 @@ export class RecordingService {
* @returns Promise
*/
async getRecordingMeetingName(): Promise {
- return invoke('get_recording_meeting_name');
+ return safeInvoke('get_recording_meeting_name');
}
/**
@@ -56,7 +57,7 @@ export class RecordingService {
* @returns Promise
*/
async startRecording(): Promise {
- return invoke('start_recording');
+ return safeInvoke('start_recording');
}
/**
@@ -71,7 +72,7 @@ export class RecordingService {
systemDeviceName: string | null,
meetingName: string
): Promise {
- return invoke('start_recording_with_devices_and_meeting', {
+ return safeInvoke('start_recording_with_devices_and_meeting', {
mic_device_name: micDeviceName,
system_device_name: systemDeviceName,
meeting_name: meetingName
@@ -84,7 +85,7 @@ export class RecordingService {
* @returns Promise
*/
async stopRecording(savePath: string): Promise {
- return invoke('stop_recording', {
+ return safeInvoke('stop_recording', {
args: { save_path: savePath }
});
}
@@ -94,7 +95,7 @@ export class RecordingService {
* @returns Promise
*/
async pauseRecording(): Promise {
- return invoke('pause_recording');
+ return safeInvoke('pause_recording');
}
/**
@@ -102,7 +103,7 @@ export class RecordingService {
* @returns Promise
*/
async resumeRecording(): Promise {
- return invoke('resume_recording');
+ return safeInvoke('resume_recording');
}
// Event Listeners
@@ -113,7 +114,7 @@ export class RecordingService {
* @returns Promise that resolves to unlisten function
*/
async onRecordingStarted(callback: () => void): Promise {
- return listen('recording-started', callback);
+ return safeListen('recording-started', callback);
}
/**
@@ -122,7 +123,7 @@ export class RecordingService {
* @returns Promise that resolves to unlisten function
*/
async onRecordingStopped(callback: (payload: RecordingStoppedPayload) => void): Promise {
- return listen('recording-stopped', (event) => {
+ return safeListen('recording-stopped', (event) => {
callback(event.payload);
});
}
@@ -133,7 +134,7 @@ export class RecordingService {
* @returns Promise that resolves to unlisten function
*/
async onRecordingPaused(callback: () => void): Promise {
- return listen('recording-paused', callback);
+ return safeListen('recording-paused', callback);
}
/**
@@ -142,7 +143,7 @@ export class RecordingService {
* @returns Promise that resolves to unlisten function
*/
async onRecordingResumed(callback: () => void): Promise {
- return listen('recording-resumed', callback);
+ return safeListen('recording-resumed', callback);
}
/**
@@ -151,7 +152,7 @@ export class RecordingService {
* @returns Promise that resolves to unlisten function
*/
async onChunkDropWarning(callback: (warning: string) => void): Promise {
- return listen('chunk-drop-warning', (event) => {
+ return safeListen('chunk-drop-warning', (event) => {
callback(event.payload);
});
}
@@ -162,7 +163,7 @@ export class RecordingService {
* @returns Promise that resolves to unlisten function
*/
async onSpeechDetected(callback: () => void): Promise {
- return listen('speech-detected', callback);
+ return safeListen('speech-detected', callback);
}
}