mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 05:12:26 +00:00
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
626 lines
20 KiB
TypeScript
626 lines
20 KiB
TypeScript
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<void> {
|
|
if (!isTauri()) return;
|
|
try {
|
|
const { invoke } = await import('@tauri-apps/api/core');
|
|
_tauriApiBase = await invoke<string>('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<T>(command: string, args: Record<string, unknown> = {}): Promise<T> {
|
|
const { invoke } = await import('@tauri-apps/api/core');
|
|
const apiUrl = getBase();
|
|
return invoke<T>(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<SetupStatus | null> {
|
|
if (!isTauri()) return null;
|
|
try {
|
|
const { invoke } = await import('@tauri-apps/api/core');
|
|
return await invoke<SetupStatus>('get_setup_status');
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// API functions
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export async function fetchModels(): Promise<ModelInfo[]> {
|
|
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<void> {
|
|
// 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<void> {
|
|
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<void> {
|
|
// 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<SavingsData> {
|
|
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<ServerInfo> {
|
|
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<boolean> {
|
|
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<unknown> {
|
|
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<unknown> {
|
|
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<unknown> {
|
|
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<TranscriptionResult> {
|
|
if (isTauri()) {
|
|
try {
|
|
const buffer = await audioBlob.arrayBuffer();
|
|
return await tauriInvoke<TranscriptionResult>('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<SpeechHealth> {
|
|
if (isTauri()) {
|
|
try {
|
|
return await tauriInvoke<SpeechHealth>('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<string, unknown>;
|
|
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<string, unknown>;
|
|
findings: unknown[];
|
|
created_at: number;
|
|
}
|
|
|
|
export interface ChannelBinding {
|
|
id: string;
|
|
agent_id: string;
|
|
channel_type: string;
|
|
config: Record<string, unknown>;
|
|
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<ManagedAgent[]> {
|
|
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<ManagedAgent> {
|
|
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<string, unknown>;
|
|
}): Promise<ManagedAgent> {
|
|
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<string, unknown> }>,
|
|
): Promise<ManagedAgent> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<AgentTask[]> {
|
|
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<AgentTask> {
|
|
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<ChannelBinding[]> {
|
|
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<AgentTemplate[]> {
|
|
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<void> {
|
|
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<AgentMessage> {
|
|
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<AgentMessage[]> {
|
|
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<ManagedAgent[]> {
|
|
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<string, unknown>;
|
|
created_at: number;
|
|
}
|
|
|
|
export interface AgentTrace {
|
|
id: string;
|
|
outcome: string;
|
|
duration: number;
|
|
started_at: number;
|
|
steps: number;
|
|
error_message?: string;
|
|
metadata?: Record<string, unknown>;
|
|
}
|
|
|
|
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<ToolInfo[]> {
|
|
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<string, string>,
|
|
): Promise<void> {
|
|
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<string, unknown>;
|
|
}>;
|
|
}
|
|
|
|
export async function fetchLearningLog(agentId: string): Promise<LearningLogEntry[]> {
|
|
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<void> {
|
|
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<AgentTrace[]> {
|
|
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<AgentTraceDetail> {
|
|
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<boolean> {
|
|
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;
|
|
}
|
|
}
|