const API_BASE = import.meta.env.VITE_API_URL ?? ""; export interface ChatSession { id: number; title: string; created_at: string; updated_at: string; } export interface ChatMessage { id: number; role: string; content: string; created_at: string; } export interface SessionDetail extends ChatSession { messages: ChatMessage[]; } export interface PomodoroCycle { completed_work_sessions: number; sessions_until_long_break: number; task_note: string; work_duration_min: number; short_break_min: number; long_break_min: number; auto_advance: boolean; chat_notify_seq: number; } export interface PomodoroStatus { status: string; phase: string; duration_min: number; task_note: string; elapsed_seconds: number; remaining_seconds: number; session_id: number | null; started_at?: string | null; finished_at?: string | null; cycle: PomodoroCycle; } export interface CharacterCardData { name: string; description: string; personality: string; scenario: string; first_mes: string; mes_example: string; system_prompt: string; post_history_instructions: string; tags: string[]; creator: string; creator_notes: string; alternate_greetings: string[]; character_version: string; } export interface CharacterCardV2 { spec: string; spec_version: string; data: CharacterCardData; } export interface UserProfile { name?: string; age?: string; timezone?: string; language?: string; notes?: string; } export interface MemoryFact { id: number; category: string; content: string; importance: number; source?: string; updated_at?: string | null; } export interface FitnessComputed { bmr: number; tdee: number; bmi: number; } export interface FitnessProfile { sex?: string; age?: number; height_cm?: number; weight_kg?: number; activity_level?: string; goal?: string; target_weight_kg?: number | null; weekly_workouts?: number; calorie_target?: number; protein_g?: number; fat_g?: number; carbs_g?: number; water_l?: number; computed?: FitnessComputed; } export interface FoodLogItem { id: number; meal_type: string; description: string; calories: number; protein_g: number; fat_g: number; carbs_g: number; estimated: boolean; logged_at?: string; } export interface WaterLogItem { id: number; amount_ml: number; logged_at?: string; } export interface WorkoutLogItem { id: number; title: string; notes?: string; duration_min?: number | null; exercises?: unknown[]; logged_at?: string; } export interface FitnessDailySummary { date: string; totals: { calories: number; protein_g: number; fat_g: number; carbs_g: number; water_ml: number; }; targets: { calories: number; protein_g: number; fat_g: number; carbs_g: number; water_ml: number; }; meals: FoodLogItem[]; water: WaterLogItem[]; workouts: WorkoutLogItem[]; } export interface BodyMetric { id: number; weight_kg: number; recorded_at?: string; } export interface FitnessReminder { id: number; kind: string; hour: number; minute: number; interval_hours?: number | null; enabled: boolean; } export interface FitnessSnapshot { profile: FitnessProfile | null; today: FitnessDailySummary; body_metrics: BodyMetric[]; reminders: FitnessReminder[]; } export interface MemorySnapshot { profile: UserProfile; facts: MemoryFact[]; session_summary?: string; total_facts: number; } export interface PomodoroHistoryItem { id: number; status: string; phase: string; duration_min: number; task_note: string; result: string | null; completed: boolean; elapsed_seconds: number; finished_at: string | null; } async function request(path: string, options?: RequestInit): Promise { const response = await fetch(`${API_BASE}${path}`, options); if (!response.ok) { const text = await response.text(); throw new Error(text || response.statusText); } return response.json() as Promise; } export const api = { health: () => request<{ status: string }>("/api/v1/health"), listSessions: () => request("/api/v1/chat/sessions"), createSession: (title = "Новый чат") => request("/api/v1/chat/sessions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title }), }), getSession: (id: number) => request(`/api/v1/chat/sessions/${id}`), deleteSession: (id: number) => request<{ ok: boolean }>(`/api/v1/chat/sessions/${id}`, { method: "DELETE" }), sendMessage: async function* (sessionId: number, content: string) { const response = await fetch(`${API_BASE}/api/v1/chat/sessions/${sessionId}/messages`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content }), }); if (!response.ok || !response.body) { throw new Error("Failed to send message"); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const parts = buffer.split("\n\n"); buffer = parts.pop() ?? ""; for (const part of parts) { const lines = part.split("\n"); let event = "message"; let data = ""; for (const line of lines) { if (line.startsWith("event: ")) event = line.slice(7); if (line.startsWith("data: ")) data = line.slice(6); } if (data) { yield { event, data: JSON.parse(data) }; } } } }, pomodoroStatus: () => request("/api/v1/pomodoro/status"), pomodoroStart: (duration_min: number, task_note: string) => request("/api/v1/pomodoro/start", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ duration_min, task_note }), }), pomodoroPause: () => request("/api/v1/pomodoro/pause", { method: "POST" }), pomodoroResume: () => request("/api/v1/pomodoro/resume", { method: "POST" }), pomodoroStop: (result: string, completed: boolean) => request("/api/v1/pomodoro/stop", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ result, completed }), }), pomodoroHistory: () => request("/api/v1/pomodoro/history"), pomodoroResetCycle: (clear_task = false) => request(`/api/v1/pomodoro/cycle/reset?clear_task=${clear_task}`, { method: "POST", }), pomodoroSkip: () => request("/api/v1/pomodoro/skip", { method: "POST" }), pomodoroStartShortBreak: (duration_min?: number) => request( `/api/v1/pomodoro/break/short/start${duration_min ? `?duration_min=${duration_min}` : ""}`, { method: "POST" } ), pomodoroStartLongBreak: (duration_min?: number) => request( `/api/v1/pomodoro/break/long/start${duration_min ? `?duration_min=${duration_min}` : ""}`, { method: "POST" } ), getCharacter: () => request("/api/v1/character"), saveCharacter: (card: CharacterCardV2) => request("/api/v1/character", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(card), }), getMemorySnapshot: (sessionId?: number) => request( `/api/v1/memory${sessionId ? `?session_id=${sessionId}` : ""}` ), updateProfile: (updates: UserProfile) => request<{ ok: boolean; profile: UserProfile }>("/api/v1/profile", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ updates }), }), createMemoryFact: (payload: { content: string; category?: string; importance?: number; session_id?: number; }) => request<{ ok: boolean; memory_id: number }>("/api/v1/memory/facts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }), forgetMemoryFact: (id: number) => request<{ ok: boolean }>(`/api/v1/memory/facts/${id}`, { method: "DELETE" }), getFitnessSnapshot: () => request("/api/v1/fitness"), updateFitnessProfile: (updates: Partial) => request<{ ok: boolean; profile: FitnessProfile }>("/api/v1/fitness/profile", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(updates), }), deleteFitnessMeal: (id: number) => request<{ ok: boolean }>(`/api/v1/fitness/meals/${id}`, { method: "DELETE" }), deleteFitnessWater: (id: number) => request<{ ok: boolean }>(`/api/v1/fitness/water/${id}`, { method: "DELETE" }), updateFitnessReminder: ( kind: string, updates: { enabled?: boolean; hour?: number; minute?: number; interval_hours?: number } ) => request<{ ok: boolean }>(`/api/v1/fitness/reminders/${kind}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(updates), }), getShoppingSnapshot: () => request("/api/v1/shopping"), createShoppingList: (name: string) => request<{ ok: boolean; list: ShoppingList }>("/api/v1/shopping/lists", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name }), }), renameShoppingList: (listId: number, name: string) => request<{ ok: boolean; list: ShoppingList }>(`/api/v1/shopping/lists/${listId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name }), }), deleteShoppingList: (listId: number) => request<{ ok: boolean }>(`/api/v1/shopping/lists/${listId}`, { method: "DELETE" }), addShoppingItems: (payload: { list_id?: number; list_name?: string; items: { text: string; quantity?: number; unit?: string }[]; }) => request<{ ok: boolean }>("/api/v1/shopping/items", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }), setShoppingItemChecked: (itemId: number, checked: boolean) => request<{ ok: boolean }>(`/api/v1/shopping/items/${itemId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ checked }), }), removeShoppingItem: (itemId: number) => request<{ ok: boolean }>(`/api/v1/shopping/items/${itemId}`, { method: "DELETE" }), clearShoppingChecked: (listId: number) => request<{ ok: boolean }>(`/api/v1/shopping/lists/${listId}/clear-checked`, { method: "POST", }), }; export interface ShoppingListItem { id: number; list_id: number; text: string; quantity: number | null; unit: string; checked: boolean; sort_order: number; } export interface ShoppingList { id: number; name: string; sort_order: number; item_count: number; unchecked_count: number; items?: ShoppingListItem[]; } export interface ShoppingSnapshot { lists: ShoppingList[]; list_count: number; total_items: number; unchecked_items: number; }