import type { ModelInfo, SavingsData, ServerInfo } from '../types'; // --------------------------------------------------------------------------- // Supabase config — safe to embed (RLS protects writes) // --------------------------------------------------------------------------- const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL || 'https://mtbtgpwzrbostweaanpr.supabase.co'; const SUPABASE_ANON_KEY = import.meta.env.VITE_SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im10YnRncHd6cmJvc3R3ZWFhbnByIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzMxODk0OTQsImV4cCI6MjA4ODc2NTQ5NH0._xMlqCfljtXpwPj54H-ghxfLFO-jiq4W2WhpU8vVL1c'; declare global { interface Window { __TAURI_INTERNALS__?: unknown; } } export const isTauri = () => typeof window !== 'undefined' && !!window.__TAURI_INTERNALS__; // Cached API base URL fetched from the Tauri backend at startup. // This avoids hardcoding the port — the Rust backend is the single // source of truth for JARVIS_PORT. let _tauriApiBase: string | null = null; /** Pre-fetch the API base URL from the Tauri backend (call once at init). */ export async function initApiBase(): Promise { if (!isTauri()) return; try { const { invoke } = await import('@tauri-apps/api/core'); _tauriApiBase = await invoke('get_api_base'); } catch { // Command may not exist on older builds; fall through to default. } } const DESKTOP_API_FALLBACK = 'http://127.0.0.1:8222'; const getSettingsApiUrl = (): string => { try { const raw = localStorage.getItem('openjarvis-settings'); if (raw) { const parsed = JSON.parse(raw); if (parsed.apiUrl) return parsed.apiUrl.replace(/\/+$/, ''); } } catch {} return ''; }; export const getBase = (): string => { const settingsUrl = getSettingsApiUrl(); if (settingsUrl) return settingsUrl; if (import.meta.env.VITE_API_URL) return import.meta.env.VITE_API_URL; if (isTauri()) return _tauriApiBase || DESKTOP_API_FALLBACK; return ''; }; async function tauriInvoke(command: string, args: Record = {}): Promise { const { invoke } = await import('@tauri-apps/api/core'); const apiUrl = getBase(); return invoke(command, { apiUrl, ...args }); } // --------------------------------------------------------------------------- // Setup status (desktop only) // --------------------------------------------------------------------------- export interface SetupStatus { phase: string; detail: string; ollama_ready: boolean; server_ready: boolean; model_ready: boolean; error: string | null; } export async function getSetupStatus(): Promise { if (!isTauri()) return null; try { const { invoke } = await import('@tauri-apps/api/core'); return await invoke('get_setup_status'); } catch { return null; } } // --------------------------------------------------------------------------- // API functions // --------------------------------------------------------------------------- export async function fetchModels(): Promise { if (isTauri()) { try { const result = await tauriInvoke<{ data?: ModelInfo[] }>('fetch_models'); return result?.data || []; } catch { // Fall through to fetch } } const res = await fetch(`${getBase()}/v1/models`); if (!res.ok) throw new Error(`Failed to fetch models: ${res.status}`); const data = await res.json(); return data.data || []; } export async function fetchRecommendedModel(): Promise<{ model: string; reason: string }> { const res = await fetch(`${getBase()}/v1/recommended-model`); if (!res.ok) return { model: '', reason: 'Failed to fetch' }; return res.json(); } export async function pullModel(modelName: string): Promise { // In Tauri, go through the Rust backend directly (avoids CORS / timeout // issues with long model downloads via fetch). if (isTauri()) { try { const { invoke } = await import('@tauri-apps/api/core'); await invoke('pull_ollama_model', { modelName }); return; } catch (e: any) { throw new Error(e?.message || e || 'Download failed'); } } const res = await fetch(`${getBase()}/v1/models/pull`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: modelName }), }); if (!res.ok) { const detail = await res.text().catch(() => res.statusText); throw new Error(`Failed to pull model: ${detail}`); } } export async function deleteModel(modelName: string): Promise { if (isTauri()) { try { const { invoke } = await import('@tauri-apps/api/core'); await invoke('delete_ollama_model', { modelName }); return; } catch (e: any) { throw new Error(e?.message || e || 'Delete failed'); } } const res = await fetch(`${getBase()}/v1/models/${encodeURIComponent(modelName)}`, { method: 'DELETE', }); if (!res.ok) { const detail = await res.text().catch(() => res.statusText); throw new Error(`Failed to delete model: ${detail}`); } } const _CLOUD_PREFIXES = ['gpt-', 'o1-', 'o3-', 'o4-', 'claude-', 'gemini-', 'openrouter/']; export async function preloadModel(modelName: string): Promise { // Cloud models don't need Ollama preloading if (_CLOUD_PREFIXES.some(p => modelName.startsWith(p))) { return; } // Trigger Ollama to load the model into memory (empty prompt, no generation). const ollamaUrl = 'http://127.0.0.1:11434'; try { const res = await fetch(`${ollamaUrl}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: modelName, prompt: '', keep_alive: '5m' }), signal: AbortSignal.timeout(120_000), }); if (!res.ok) throw new Error(`Preload failed: ${res.status}`); } catch (e: any) { if (e.name === 'TimeoutError') throw new Error('Model load timed out (120s)'); throw e; } } export async function fetchSavings(): Promise { const res = await fetch(`${getBase()}/v1/savings`); if (!res.ok) throw new Error(`Failed to fetch savings: ${res.status}`); return res.json(); } export async function fetchServerInfo(): Promise { const res = await fetch(`${getBase()}/v1/info`); if (!res.ok) throw new Error(`Failed to fetch server info: ${res.status}`); return res.json(); } export async function checkHealth(): Promise { if (isTauri()) { try { await tauriInvoke('check_health', { apiUrl: getBase() }); return true; } catch { return false; } } try { const res = await fetch(`${getBase()}/health`); return res.ok; } catch { return false; } } export async function fetchEnergy(): Promise { if (isTauri()) { try { return await tauriInvoke('fetch_energy', { apiUrl: getBase() }); } catch {} } const res = await fetch(`${getBase()}/v1/telemetry/energy`); if (!res.ok) throw new Error(`Failed: ${res.status}`); return res.json(); } export async function fetchTelemetry(): Promise { if (isTauri()) { try { return await tauriInvoke('fetch_telemetry', { apiUrl: getBase() }); } catch {} } const res = await fetch(`${getBase()}/v1/telemetry/stats`); if (!res.ok) throw new Error(`Failed: ${res.status}`); return res.json(); } export async function fetchTraces(limit: number = 50): Promise { if (isTauri()) { try { return await tauriInvoke('fetch_traces', { apiUrl: getBase(), limit }); } catch {} } const res = await fetch(`${getBase()}/v1/traces?limit=${limit}`); if (!res.ok) throw new Error(`Failed: ${res.status}`); return res.json(); } // --------------------------------------------------------------------------- // Speech // --------------------------------------------------------------------------- export interface TranscriptionResult { text: string; language: string | null; confidence: number | null; duration_seconds: number; } export interface SpeechHealth { available: boolean; backend?: string; reason?: string; } export async function transcribeAudio(audioBlob: Blob, filename = 'recording.webm'): Promise { if (isTauri()) { try { const buffer = await audioBlob.arrayBuffer(); return await tauriInvoke('transcribe_audio', { audioData: Array.from(new Uint8Array(buffer)), filename, }); } catch { // Fall through to fetch } } const formData = new FormData(); formData.append('file', audioBlob, filename); const res = await fetch(`${getBase()}/v1/speech/transcribe`, { method: 'POST', body: formData, }); if (!res.ok) throw new Error(`Transcription failed: ${res.status}`); return res.json(); } export async function fetchSpeechHealth(): Promise { if (isTauri()) { try { return await tauriInvoke('speech_health'); } catch { return { available: false }; } } const res = await fetch(`${getBase()}/v1/speech/health`); if (!res.ok) return { available: false }; return res.json(); } // --------------------------------------------------------------------------- // Agent Manager // --------------------------------------------------------------------------- export interface ManagedAgent { id: string; name: string; agent_type: string; config: Record; status: 'idle' | 'running' | 'paused' | 'error' | 'archived' | 'needs_attention' | 'budget_exceeded' | 'stalled'; summary_memory: string; created_at: number; updated_at: number; // Runtime stats total_runs?: number; total_cost?: number; total_tokens?: number; input_tokens?: number; output_tokens?: number; last_run_at?: number | null; // Schedule schedule_type?: string; schedule_value?: string; // Budget budget?: number; // Learning learning_enabled?: boolean; // Live progress current_activity?: string; } export interface AgentTask { id: string; agent_id: string; description: string; status: 'pending' | 'active' | 'completed' | 'failed'; progress: Record; findings: unknown[]; created_at: number; } export interface ChannelBinding { id: string; agent_id: string; channel_type: string; config: Record; session_id: string; routing_mode: string; } export interface AgentTemplate { id: string; name: string; description: string; source: 'built-in' | 'user'; agent_type: string; [key: string]: unknown; } export interface AgentMessage { id: string; agent_id: string; direction: 'user_to_agent' | 'agent_to_user'; content: string; mode: 'immediate' | 'queued'; status: 'pending' | 'delivered' | 'responded'; created_at: number; } export async function fetchManagedAgents(): Promise { const res = await fetch(`${getBase()}/v1/managed-agents`); if (!res.ok) throw new Error(`Failed: ${res.status}`); const data = await res.json(); return data.agents || []; } export async function fetchManagedAgent(agentId: string): Promise { const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}`); if (!res.ok) throw new Error(`Failed: ${res.status}`); return res.json(); } export async function createManagedAgent(body: { name: string; agent_type?: string; template_id?: string; config?: Record; }): Promise { const res = await fetch(`${getBase()}/v1/managed-agents`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`Failed: ${res.status}`); return res.json(); } export async function updateManagedAgent( agentId: string, body: Partial<{ name: string; agent_type: string; config: Record }>, ): Promise { const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`Failed: ${res.status}`); return res.json(); } export async function deleteManagedAgent(agentId: string): Promise { const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}`, { method: 'DELETE' }); if (!res.ok) throw new Error(`Failed: ${res.status}`); } export async function pauseManagedAgent(agentId: string): Promise { const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/pause`, { method: 'POST' }); if (!res.ok) throw new Error(`Failed: ${res.status}`); } export async function resumeManagedAgent(agentId: string): Promise { const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/resume`, { method: 'POST' }); if (!res.ok) throw new Error(`Failed: ${res.status}`); } export async function fetchAgentTasks(agentId: string): Promise { const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/tasks`); if (!res.ok) throw new Error(`Failed: ${res.status}`); const data = await res.json(); return data.tasks || []; } export async function createAgentTask(agentId: string, description: string): Promise { const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/tasks`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ description }), }); if (!res.ok) throw new Error(`Failed: ${res.status}`); return res.json(); } export async function fetchAgentChannels(agentId: string): Promise { const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/channels`); if (!res.ok) throw new Error(`Failed: ${res.status}`); const data = await res.json(); return data.bindings || []; } export async function fetchTemplates(): Promise { const res = await fetch(`${getBase()}/v1/templates`); if (!res.ok) throw new Error(`Failed: ${res.status}`); const data = await res.json(); return data.templates || []; } export async function runManagedAgent(agentId: string): Promise { const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/run`, { method: 'POST' }); if (!res.ok) { const body = await res.json().catch(() => ({ detail: res.statusText })); throw new Error(body.detail || `Failed: ${res.status}`); } } export async function recoverManagedAgent(agentId: string): Promise<{ recovered: boolean; checkpoint: unknown }> { const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/recover`, { method: 'POST' }); if (!res.ok) { const body = await res.json().catch(() => ({ detail: res.statusText })); throw new Error(body.detail || `Failed: ${res.status}`); } return res.json(); } export async function fetchAgentState(agentId: string): Promise<{ agent: ManagedAgent; tasks: AgentTask[]; channels: ChannelBinding[]; messages: AgentMessage[]; checkpoint: unknown; }> { const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/state`); if (!res.ok) throw new Error(`Failed: ${res.status}`); return res.json(); } export async function sendAgentMessage(agentId: string, content: string, mode: 'immediate' | 'queued' = 'queued'): Promise { const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content, mode }), }); if (!res.ok) throw new Error(`Failed: ${res.status}`); return res.json(); } export async function fetchAgentMessages(agentId: string): Promise { const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/messages`); if (!res.ok) throw new Error(`Failed: ${res.status}`); const data = await res.json(); return data.messages || []; } export async function fetchErrorAgents(): Promise { const res = await fetch(`${getBase()}/v1/agents/errors`); if (!res.ok) throw new Error(`Failed: ${res.status}`); const data = await res.json(); return data.agents || []; } // --------------------------------------------------------------------------- // Agent Learning + Traces // --------------------------------------------------------------------------- export interface LearningLogEntry { id: string; agent_id: string; event_type: string; description: string; data: Record; created_at: number; } export interface AgentTrace { id: string; outcome: string; duration: number; started_at: number; steps: number; error_message?: string; metadata?: Record; } export interface ToolInfo { name: string; description: string; category: string; source: 'tool' | 'channel'; requires_credentials: boolean; credential_keys: string[]; configured: boolean; } export async function fetchAvailableTools(): Promise { const res = await fetch(`${getBase()}/v1/tools`); if (!res.ok) throw new Error(`Failed: ${res.status}`); const data = await res.json(); return data.tools || []; } export async function saveToolCredentials( toolName: string, credentials: Record, ): Promise { const res = await fetch(`${getBase()}/v1/tools/${toolName}/credentials`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials), }); if (!res.ok) throw new Error(`Failed: ${res.status}`); } export interface AgentTraceDetail { id: string; agent: string; outcome: string; duration: number; started_at: number; steps: Array<{ step_type: string; input: unknown; output: string; duration: number; metadata: Record; }>; } export async function fetchLearningLog(agentId: string): Promise { const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/learning`); if (!res.ok) throw new Error(`Failed: ${res.status}`); const data = await res.json(); return data.learning_log || []; } export async function triggerLearning(agentId: string): Promise { const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/learning/run`, { method: 'POST' }); if (!res.ok) throw new Error(`Failed: ${res.status}`); } export async function fetchAgentTraces(agentId: string, limit = 20): Promise { const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/traces?limit=${limit}`); if (!res.ok) throw new Error(`Failed: ${res.status}`); const data = await res.json(); return data.traces || []; } export async function fetchAgentTrace(agentId: string, traceId: string): Promise { const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/traces/${traceId}`); if (!res.ok) throw new Error(`Failed: ${res.status}`); return res.json(); } // --------------------------------------------------------------------------- // Leaderboard savings submission (Supabase) // --------------------------------------------------------------------------- export interface SavingsSubmission { anon_id: string; display_name: string; email: string; total_calls: number; total_tokens: number; dollar_savings: number; energy_wh_saved: number; flops_saved: number; token_counting_version?: number; } export async function submitSavings(data: SavingsSubmission): Promise { if (!SUPABASE_URL || !SUPABASE_ANON_KEY) return false; try { const res = await fetch( `${SUPABASE_URL}/rest/v1/savings_entries?on_conflict=anon_id`, { method: 'POST', headers: { 'Content-Type': 'application/json', apikey: SUPABASE_ANON_KEY, Authorization: `Bearer ${SUPABASE_ANON_KEY}`, Prefer: 'resolution=merge-duplicates', }, body: JSON.stringify(data), }, ); return res.ok || res.status === 201 || res.status === 200; } catch { return false; } }