Files
Synculous-2/synculous-client/src/lib/api.ts
chelsea bebc609091 Add one-off tasks/appointments feature
- DB: tasks table with scheduled_datetime, reminder_minutes_before, advance_notified, status
- API: CRUD routes GET/POST /api/tasks, PATCH/DELETE /api/tasks/<id>
- Scheduler: check_task_reminders() fires advance + at-time notifications, tracks advance_notified to prevent double-fire
- Bot: handle_task() with add/list/done/cancel/delete actions + datetime resolution helper
- AI: task interaction type + examples added to command_parser
- Web: task list page with overdue/notified color coding + new task form with datetime-local picker
- Nav: replaced Templates with Tasks in bottom nav

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 16:43:42 -06:00

1089 lines
29 KiB
TypeScript

const API_URL = '';
export interface Task {
id: string;
user_uuid: string;
title: string;
description?: string;
scheduled_datetime: string;
reminder_minutes_before: number;
advance_notified: boolean;
status: 'pending' | 'notified' | 'completed' | 'cancelled';
created_at: string;
updated_at: string;
}
function getToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('token');
}
function setToken(token: string): void {
localStorage.setItem('token', token);
}
function clearToken(): void {
localStorage.removeItem('token');
localStorage.removeItem('refresh_token');
}
function getRefreshToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('refresh_token');
}
function setRefreshToken(token: string): void {
localStorage.setItem('refresh_token', token);
}
let refreshPromise: Promise<boolean> | null = null;
async function tryRefreshToken(): Promise<boolean> {
// Deduplicate concurrent refresh attempts
if (refreshPromise) return refreshPromise;
refreshPromise = (async () => {
const refreshToken = getRefreshToken();
if (!refreshToken) return false;
try {
const resp = await fetch(`${API_URL}/api/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken }),
});
if (resp.ok) {
const data = await resp.json();
if (data.token) {
setToken(data.token);
return true;
}
}
// Refresh token is invalid/expired - clear everything
clearToken();
return false;
} catch {
return false;
} finally {
refreshPromise = null;
}
})();
return refreshPromise;
}
async function request<T>(
endpoint: string,
options: RequestInit = {},
_retried = false,
): Promise<T> {
const token = getToken();
const headers: HeadersInit = {
'Content-Type': 'application/json',
'X-Timezone-Offset': String(new Date().getTimezoneOffset()),
'X-Timezone-Name': Intl.DateTimeFormat().resolvedOptions().timeZone,
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options.headers,
};
const response = await fetch(`${API_URL}${endpoint}`, {
...options,
headers,
});
// Auto-refresh on 401
if (response.status === 401 && !_retried) {
const refreshed = await tryRefreshToken();
if (refreshed) {
return request<T>(endpoint, options, true);
}
}
if (!response.ok) {
const body = await response.text();
let errorMsg = 'Request failed';
try {
const error = JSON.parse(body);
errorMsg = error.error || error.message || body;
} catch {
errorMsg = body || errorMsg;
}
throw new Error(errorMsg);
}
return response.json();
}
export const api = {
// Auth
auth: {
login: async (username: string, password: string, trustDevice = false) => {
const result = await request<{ token: string; refresh_token?: string }>('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password, trust_device: trustDevice }),
});
setToken(result.token);
if (result.refresh_token) {
setRefreshToken(result.refresh_token);
}
return result;
},
register: async (username: string, password: string) => {
return request<{ success: boolean }>('/api/register', {
method: 'POST',
body: JSON.stringify({ username, password }),
});
},
logout: () => {
clearToken();
},
getToken,
},
// User
user: {
get: async (userId: string) => {
return request<{ id: string; username: string; created_at: string }>(
`/api/user/${userId}`,
{ method: 'GET' }
);
},
getUUID: async (username: string) => {
return request<{ id: string }>(`/api/getUserUUID/${username}`, {
method: 'GET',
});
},
update: async (userId: string, data: Record<string, unknown>) => {
return request<{ success: boolean }>(`/api/user/${userId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
},
},
// Routines
routines: {
list: async () => {
return request<Array<{
id: string;
user_uuid: string;
name: string;
description?: string;
icon?: string;
created_at: string;
}>>('/api/routines', { method: 'GET' });
},
get: async (routineId: string) => {
return request<{
routine: {
id: string;
user_uuid: string;
name: string;
description?: string;
icon?: string;
};
steps: Array<{
id: string;
routine_id: string;
name: string;
instructions?: string;
step_type: string;
duration_minutes?: number;
media_url?: string;
position: number;
}>;
}>(`/api/routines/${routineId}`, { method: 'GET' });
},
create: async (data: { name: string; description?: string; icon?: string }) => {
return request<{ id: string }>('/api/routines', {
method: 'POST',
body: JSON.stringify(data),
});
},
update: async (routineId: string, data: Record<string, unknown>) => {
return request<{ id: string }>(`/api/routines/${routineId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
},
delete: async (routineId: string) => {
return request<{ deleted: boolean }>(`/api/routines/${routineId}`, {
method: 'DELETE',
});
},
// Steps
getSteps: async (routineId: string) => {
return request<Array<{
id: string;
routine_id: string;
name: string;
instructions?: string;
step_type: string;
duration_minutes?: number;
media_url?: string;
position: number;
}>>(`/api/routines/${routineId}/steps`, { method: 'GET' });
},
addStep: async (
routineId: string,
data: { name: string; duration_minutes?: number; position?: number }
) => {
return request<{ id: string }>(`/api/routines/${routineId}/steps`, {
method: 'POST',
body: JSON.stringify(data),
});
},
updateStep: async (
routineId: string,
stepId: string,
data: Record<string, unknown>
) => {
return request<{ id: string }>(`/api/routines/${routineId}/steps/${stepId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
},
deleteStep: async (routineId: string, stepId: string) => {
return request<{ deleted: boolean }>(
`/api/routines/${routineId}/steps/${stepId}`,
{ method: 'DELETE' }
);
},
reorderSteps: async (routineId: string, stepIds: string[]) => {
return request<Array<{ id: string }>>(
`/api/routines/${routineId}/steps/reorder`,
{
method: 'PUT',
body: JSON.stringify({ step_ids: stepIds }),
}
);
},
// Step extended
updateStepInstructions: async (
routineId: string,
stepId: string,
instructions: string
) => {
return request<{ id: string }>(
`/api/routines/${routineId}/steps/${stepId}/instructions`,
{
method: 'PUT',
body: JSON.stringify({ instructions }),
}
);
},
updateStepType: async (
routineId: string,
stepId: string,
stepType: string
) => {
return request<{ id: string }>(
`/api/routines/${routineId}/steps/${stepId}/type`,
{
method: 'PUT',
body: JSON.stringify({ step_type: stepType }),
}
);
},
updateStepMedia: async (
routineId: string,
stepId: string,
mediaUrl: string
) => {
return request<{ id: string }>(
`/api/routines/${routineId}/steps/${stepId}/media`,
{
method: 'PUT',
body: JSON.stringify({ media_url: mediaUrl }),
}
);
},
// Scheduling
getSchedule: async (routineId: string) => {
return request<{
id: string;
routine_id: string;
days: string[];
time: string;
remind: boolean;
}>(`/api/routines/${routineId}/schedule`, { method: 'GET' });
},
setSchedule: async (
routineId: string,
data: { days: string[]; time: string; remind?: boolean }
) => {
return request<{ id: string }>(`/api/routines/${routineId}/schedule`, {
method: 'PUT',
body: JSON.stringify(data),
});
},
deleteSchedule: async (routineId: string) => {
return request<{ deleted: boolean }>(
`/api/routines/${routineId}/schedule`,
{ method: 'DELETE' }
);
},
listAllSchedules: async () => {
return request<Array<{
routine_id: string;
routine_name: string;
routine_icon: string;
days: string[];
time: string;
remind: boolean;
total_duration_minutes: number;
}>>('/api/routines/schedules', { method: 'GET' });
},
// History
getHistory: async (routineId: string, days = 7) => {
return request<Array<{
id: string;
routine_id: string;
status: string;
created_at: string;
completed_at?: string;
}>>(`/api/routines/${routineId}/history?days=${days}`, {
method: 'GET',
});
},
// Stats
getStats: async (routineId: string, days = 30) => {
return request<{
routine_id: string;
routine_name: string;
period_days: number;
total_sessions: number;
completed: number;
aborted: number;
completion_rate_percent: number;
avg_duration_minutes: number;
total_time_minutes: number;
}>(`/api/routines/${routineId}/stats?days=${days}`, { method: 'GET' });
},
getStreak: async (routineId: string) => {
return request<{
routine_id: string;
routine_name: string;
current_streak: number;
longest_streak: number;
last_completed_date?: string;
}>(`/api/routines/${routineId}/streak`, { method: 'GET' });
},
// Tags
getTags: async (routineId: string) => {
return request<Array<{ id: string; name: string; color: string }>>(
`/api/routines/${routineId}/tags`,
{ method: 'GET' }
);
},
addTags: async (routineId: string, tagIds: string[]) => {
return request<Array<{ id: string; name: string; color: string }>>(
`/api/routines/${routineId}/tags`,
{
method: 'POST',
body: JSON.stringify({ tag_ids: tagIds }),
}
);
},
removeTag: async (routineId: string, tagId: string) => {
return request<{ removed: boolean }>(
`/api/routines/${routineId}/tags/${tagId}`,
{ method: 'DELETE' }
);
},
},
// Sessions
sessions: {
start: async (routineId: string) => {
return request<{
session: { id: string; status: string };
current_step: {
id: string;
name: string;
instructions?: string;
step_type: string;
duration_minutes?: number;
position: number;
};
}>(`/api/routines/${routineId}/start`, { method: 'POST' });
},
getActive: async () => {
return request<{
session: {
id: string;
routine_id: string;
status: string;
current_step_index: number;
};
routine: { id: string; name: string; icon?: string };
current_step: {
id: string;
name: string;
instructions?: string;
step_type: string;
duration_minutes?: number;
position: number;
} | null;
}>('/api/sessions/active', { method: 'GET' });
},
completeStep: async (sessionId: string, stepId: string) => {
return request<{
session: { status: string; current_step_index?: number };
next_step: {
id: string;
name: string;
instructions?: string;
step_type: string;
duration_minutes?: number;
position: number;
} | null;
celebration?: {
streak_current: number;
streak_longest: number;
session_duration_minutes: number;
total_completions: number;
steps_completed: number;
steps_skipped: number;
};
}>(`/api/sessions/${sessionId}/complete-step`, {
method: 'POST',
body: JSON.stringify({ step_id: stepId }),
});
},
skipStep: async (sessionId: string, stepId: string) => {
return request<{
session: { status: string; current_step_index?: number };
next_step: {
id: string;
name: string;
instructions?: string;
step_type: string;
duration_minutes?: number;
position: number;
} | null;
celebration?: {
streak_current: number;
streak_longest: number;
session_duration_minutes: number;
total_completions: number;
steps_completed: number;
steps_skipped: number;
};
}>(`/api/sessions/${sessionId}/skip-step`, {
method: 'POST',
body: JSON.stringify({ step_id: stepId }),
});
},
pause: async (sessionId: string) => {
return request<{ status: string }>(`/api/sessions/${sessionId}/pause`, {
method: 'POST',
});
},
resume: async (sessionId: string) => {
return request<{ status: string }>(`/api/sessions/${sessionId}/resume`, {
method: 'POST',
});
},
cancel: async (sessionId: string) => {
return request<{ status: string }>(`/api/sessions/${sessionId}/cancel`, {
method: 'POST',
});
},
abort: async (sessionId: string, reason?: string) => {
return request<{ status: string; reason: string }>(
`/api/sessions/${sessionId}/abort`,
{
method: 'POST',
body: JSON.stringify({ reason }),
}
);
},
addNote: async (
sessionId: string,
stepIndex: number | undefined,
note: string
) => {
return request<{ id: string }>(`/api/sessions/${sessionId}/note`, {
method: 'POST',
body: JSON.stringify({ step_index: stepIndex, note }),
});
},
setDuration: async (sessionId: string, durationMinutes: number) => {
return request<{ id: string }>(`/api/sessions/${sessionId}/duration`, {
method: 'PUT',
body: JSON.stringify({ actual_duration_minutes: durationMinutes }),
});
},
getDetails: async (sessionId: string) => {
return request<{
session: {
id: string;
routine_id: string;
status: string;
current_step_index: number;
};
routine: { id: string; name: string };
steps: Array<{ id: string; name: string; position: number }>;
notes: Array<{ id: string; step_index?: number; note: string }>;
}>(`/api/sessions/${sessionId}`, { method: 'GET' });
},
},
// Templates
templates: {
list: async () => {
return request<Array<{
id: string;
name: string;
description?: string;
icon?: string;
step_count: number;
}>>('/api/templates', { method: 'GET' });
},
get: async (templateId: string) => {
return request<{
template: {
id: string;
name: string;
description?: string;
icon?: string;
};
steps: Array<{
id: string;
name: string;
instructions?: string;
step_type: string;
duration_minutes?: number;
position: number;
}>;
}>(`/api/templates/${templateId}`, { method: 'GET' });
},
clone: async (templateId: string) => {
return request<{ id: string }>(`/api/templates/${templateId}/clone`, {
method: 'POST',
});
},
},
// Tags
tags: {
list: async () => {
return request<Array<{ id: string; name: string; color: string }>>(
'/api/tags',
{ method: 'GET' }
);
},
create: async (data: { name: string; color?: string }) => {
return request<{ id: string }>('/api/tags', {
method: 'POST',
body: JSON.stringify(data),
});
},
delete: async (tagId: string) => {
return request<{ deleted: boolean }>(`/api/tags/${tagId}`, {
method: 'DELETE',
});
},
},
// Stats
stats: {
getWeeklySummary: async () => {
return request<{
total_completed: number;
total_time_minutes: number;
routines_started: number;
routines: Array<{
routine_id: string;
name: string;
completed_this_week: number;
}>;
}>('/api/routines/weekly-summary', { method: 'GET' });
},
getStreaks: async () => {
return request<Array<{
routine_id: string;
routine_name: string;
current_streak: number;
longest_streak: number;
last_completed_date?: string;
}>>('/api/routines/streaks', { method: 'GET' });
},
},
// Victories
victories: {
get: async (days = 30) => {
return request<Array<{
type: string;
message: string;
date?: string;
}>>(`/api/victories?days=${days}`, { method: 'GET' });
},
},
// Rewards
rewards: {
getRandom: async (context = 'completion') => {
return request<{
reward: {
id: string;
category: string;
content: string;
emoji?: string;
rarity: string;
} | null;
}>(`/api/rewards/random?context=${context}`, { method: 'GET' });
},
getHistory: async () => {
return request<Array<{
earned_at: string;
context?: string;
category: string;
content: string;
emoji?: string;
rarity?: string;
}>>('/api/rewards/history', { method: 'GET' });
},
},
// Preferences
preferences: {
get: async () => {
return request<{
sound_enabled: boolean;
haptic_enabled: boolean;
show_launch_screen: boolean;
celebration_style: string;
}>('/api/preferences', { method: 'GET' });
},
update: async (data: {
sound_enabled?: boolean;
haptic_enabled?: boolean;
show_launch_screen?: boolean;
celebration_style?: string;
timezone_offset?: number;
timezone_name?: string;
}) => {
return request<Record<string, unknown>>('/api/preferences', {
method: 'PUT',
body: JSON.stringify(data),
});
},
},
// Notifications
notifications: {
getVapidPublicKey: async () => {
return request<{ public_key: string }>('/api/notifications/vapid-public-key', {
method: 'GET',
});
},
subscribe: async (subscription: PushSubscriptionJSON) => {
return request<{ subscribed: boolean }>('/api/notifications/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
});
},
unsubscribe: async (endpoint: string) => {
return request<{ unsubscribed: boolean }>('/api/notifications/subscribe', {
method: 'DELETE',
body: JSON.stringify({ endpoint }),
});
},
getSettings: async () => {
return request<{
discord_user_id: string;
discord_enabled: boolean;
ntfy_topic: string;
ntfy_enabled: boolean;
web_push_enabled: boolean;
}>('/api/notifications/settings', { method: 'GET' });
},
updateSettings: async (data: {
discord_user_id?: string;
discord_enabled?: boolean;
ntfy_topic?: string;
ntfy_enabled?: boolean;
}) => {
return request<{ updated: boolean }>('/api/notifications/settings', {
method: 'PUT',
body: JSON.stringify(data),
});
},
},
// Adaptive Medications
adaptiveMeds: {
getSettings: async () => {
return request<{
adaptive_timing_enabled: boolean;
adaptive_mode: string;
presence_tracking_enabled: boolean;
nagging_enabled: boolean;
nag_interval_minutes: number;
max_nag_count: number;
quiet_hours_start: string | null;
quiet_hours_end: string | null;
}>('/api/adaptive-meds/settings', { method: 'GET' });
},
updateSettings: async (data: {
adaptive_timing_enabled?: boolean;
adaptive_mode?: string;
presence_tracking_enabled?: boolean;
nagging_enabled?: boolean;
nag_interval_minutes?: number;
max_nag_count?: number;
quiet_hours_start?: string | null;
quiet_hours_end?: string | null;
}) => {
return request<{ success: boolean }>('/api/adaptive-meds/settings', {
method: 'PUT',
body: JSON.stringify(data),
});
},
getPresence: async () => {
return request<{
is_online: boolean;
last_online_at: string | null;
typical_wake_time: string | null;
}>('/api/adaptive-meds/presence', { method: 'GET' });
},
getSchedule: async () => {
return request<Array<{
medication_id: string;
medication_name: string;
base_time: string;
adjusted_time: string;
adjustment_minutes: number;
status: string;
nag_count: number;
}>>('/api/adaptive-meds/schedule', { method: 'GET' });
},
},
// Snitch System
snitch: {
getSettings: async () => {
return request<{
snitch_enabled: boolean;
trigger_after_nags: number;
trigger_after_missed_doses: number;
max_snitches_per_day: number;
require_consent: boolean;
consent_given: boolean;
snitch_cooldown_hours: number;
}>('/api/snitch/settings', { method: 'GET' });
},
updateSettings: async (data: {
snitch_enabled?: boolean;
trigger_after_nags?: number;
trigger_after_missed_doses?: number;
max_snitches_per_day?: number;
require_consent?: boolean;
consent_given?: boolean;
snitch_cooldown_hours?: number;
}) => {
return request<{ success: boolean }>('/api/snitch/settings', {
method: 'PUT',
body: JSON.stringify(data),
});
},
giveConsent: async (consent_given: boolean) => {
return request<{ success: boolean; consent_given: boolean }>('/api/snitch/consent', {
method: 'POST',
body: JSON.stringify({ consent_given }),
});
},
getContacts: async () => {
return request<Array<{
id: string;
contact_name: string;
contact_type: string;
contact_value: string;
priority: number;
notify_all: boolean;
is_active: boolean;
}>>('/api/snitch/contacts', { method: 'GET' });
},
addContact: async (data: {
contact_name: string;
contact_type: string;
contact_value: string;
priority?: number;
notify_all?: boolean;
is_active?: boolean;
}) => {
return request<{ success: boolean; contact_id: string }>('/api/snitch/contacts', {
method: 'POST',
body: JSON.stringify(data),
});
},
updateContact: async (contactId: string, data: {
contact_name?: string;
contact_type?: string;
contact_value?: string;
priority?: number;
notify_all?: boolean;
is_active?: boolean;
}) => {
return request<{ success: boolean }>(`/api/snitch/contacts/${contactId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
},
deleteContact: async (contactId: string) => {
return request<{ success: boolean }>(`/api/snitch/contacts/${contactId}`, {
method: 'DELETE',
});
},
getHistory: async (days?: number) => {
return request<Array<{
id: string;
contact_id: string;
medication_id: string;
trigger_reason: string;
snitch_count_today: number;
sent_at: string;
delivered: boolean;
}>>(`/api/snitch/history?days=${days || 7}`, { method: 'GET' });
},
test: async () => {
return request<{ success: boolean; message: string }>('/api/snitch/test', {
method: 'POST',
});
},
},
// Medications
medications: {
list: async () => {
return request<Array<{
id: string;
name: string;
dosage: string;
unit: string;
frequency: string;
times: string[];
notes?: string;
active: boolean;
quantity_remaining?: number;
refill_date?: string;
}>>('/api/medications', { method: 'GET' });
},
get: async (medId: string) => {
return request<{
id: string;
name: string;
dosage: string;
unit: string;
frequency: string;
times: string[];
notes?: string;
active: boolean;
quantity_remaining?: number;
refill_date?: string;
}>(`/api/medications/${medId}`, { method: 'GET' });
},
create: async (data: {
name: string;
dosage: string;
unit: string;
frequency: string;
times?: string[];
days_of_week?: string[];
interval_days?: number;
start_date?: string;
notes?: string;
}) => {
return request<{ id: string }>('/api/medications', {
method: 'POST',
body: JSON.stringify(data),
});
},
update: async (medId: string, data: Record<string, unknown>) => {
return request<{ id: string }>(`/api/medications/${medId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
},
delete: async (medId: string) => {
return request<{ deleted: boolean }>(`/api/medications/${medId}`, {
method: 'DELETE',
});
},
take: async (medId: string, scheduledTime?: string, notes?: string) => {
return request<{ id: string }>(`/api/medications/${medId}/take`, {
method: 'POST',
body: JSON.stringify({ scheduled_time: scheduledTime, notes }),
});
},
skip: async (medId: string, scheduledTime?: string, reason?: string) => {
return request<{ id: string }>(`/api/medications/${medId}/skip`, {
method: 'POST',
body: JSON.stringify({ scheduled_time: scheduledTime, reason }),
});
},
snooze: async (medId: string, minutes = 15) => {
return request<{ snoozed_until_minutes: number }>(
`/api/medications/${medId}/snooze`,
{
method: 'POST',
body: JSON.stringify({ minutes }),
}
);
},
getLog: async (medId: string, days = 30) => {
return request<Array<{
id: string;
medication_id: string;
action: string;
scheduled_time?: string;
notes?: string;
created_at: string;
}>>(`/api/medications/${medId}/log?days=${days}`, { method: 'GET' });
},
getToday: async () => {
return request<Array<{
medication: {
id: string;
name: string;
dosage: string;
unit: string;
};
scheduled_times: string[];
taken_times: string[];
skipped_times?: string[];
is_prn?: boolean;
is_next_day?: boolean;
is_previous_day?: boolean;
}>>('/api/medications/today', { method: 'GET' });
},
getAdherence: async (days = 30) => {
return request<Array<{
medication_id: string;
name: string;
taken: number;
skipped: number;
adherence_percent: number;
}>>(`/api/medications/adherence?days=${days}`, { method: 'GET' });
},
getRefillsDue: async (daysAhead = 7) => {
return request<Array<{
id: string;
name: string;
dosage: string;
quantity_remaining?: number;
refill_date?: string;
}>>(`/api/medications/refills-due?days_ahead=${daysAhead}`, {
method: 'GET',
});
},
setRefill: async (
medId: string,
data: {
quantity_remaining?: number;
refill_date?: string;
pharmacy_notes?: string;
}
) => {
return request<{ id: string }>(`/api/medications/${medId}/refill`, {
method: 'PUT',
body: JSON.stringify(data),
});
},
},
tasks: {
list: (status = 'pending') =>
request<Task[]>(`/api/tasks?status=${status}`, { method: 'GET' }),
create: (data: { title: string; description?: string; scheduled_datetime: string; reminder_minutes_before?: number }) =>
request<Task>('/api/tasks', { method: 'POST', body: JSON.stringify(data) }),
update: (id: string, data: Partial<Pick<Task, 'title' | 'description' | 'scheduled_datetime' | 'reminder_minutes_before' | 'status'>>) =>
request<Task>(`/api/tasks/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
delete: (id: string) =>
request<{ success: boolean }>(`/api/tasks/${id}`, { method: 'DELETE' }),
},
ai: {
generateSteps: (goal: string) =>
request<{ steps: { name: string; duration_minutes: number }[] }>(
'/api/ai/generate-steps',
{ method: 'POST', body: JSON.stringify({ goal }) }
),
},
};
export default api;