peregrine/web/src/composables/useApi.ts
pyr0ball 53b07568d9 feat(vue): accumulated parity work — Q&A, Apply highlights, AppNav switcher, cloud API
API additions (dev-api.py split across this and next commit):
- /api/jobs/{job_id}/qa GET/PATCH/suggest — Interview Prep answer storage + LLM suggestions
- /api/settings/ui-preference POST — persist streamlit/vue preference to user.yaml
- cancel_task() added to scripts/db.py (per-task cancel for Danger Zone)

Vue / UI:
- AppNav: " Classic" button to switch back to Streamlit UI (writes cookie + persists to user.yaml)
- ApplyWorkspace: Resume Highlights panel (collapsible skills/domains/keywords with job-match highlighting)
- SettingsView: hide Data tab in cloud mode (showData guard)
- ResumeProfileView: minor improvements
- useApi.ts: error handling improvements

Infra:
- compose.cloud.yml: add api service (uvicorn dev_api running in cloud container)
- docker/web/nginx.conf: proxy /api/* to api service in cloud mode
- README.md: Vue SPA now listed as Free tier feature
2026-04-04 22:04:51 -07:00

53 lines
1.5 KiB
TypeScript

export type ApiError =
| { kind: 'network'; message: string }
| { kind: 'http'; status: number; detail: string }
// Strip trailing slash so '/peregrine/' + '/api/...' → '/peregrine/api/...'
const _apiBase = import.meta.env.BASE_URL.replace(/\/$/, '')
export async function useApiFetch<T>(
url: string,
opts?: RequestInit,
): Promise<{ data: T | null; error: ApiError | null }> {
try {
const res = await fetch(_apiBase + url, opts)
if (!res.ok) {
const detail = await res.text().catch(() => '')
return { data: null, error: { kind: 'http', status: res.status, detail } }
}
const data = await res.json() as T
return { data, error: null }
} catch (e) {
return { data: null, error: { kind: 'network', message: String(e) } }
}
}
/**
* Open an SSE connection. Returns a cleanup function.
* onEvent receives each parsed JSON payload.
* onComplete is called when the server sends a {"type":"complete"} event.
* onError is called on connection error.
*/
export function useApiSSE(
url: string,
onEvent: (data: Record<string, unknown>) => void,
onComplete?: () => void,
onError?: (e: Event) => void,
): () => void {
const es = new EventSource(_apiBase + url)
es.onmessage = (e) => {
try {
const data = JSON.parse(e.data) as Record<string, unknown>
onEvent(data)
if (data.type === 'complete') {
es.close()
onComplete?.()
}
} catch { /* ignore malformed events */ }
}
es.onerror = (e) => {
onError?.(e)
es.close()
}
return () => es.close()
}