import axios from "axios"; import type { MetaResponse, SearchRequest, SearchResponse, ChatRequest, ChatResponse, ChatMessage, ChatReply, PersonDetail, ChatSessionData, SessionUpsertData, SnapshotData, SnapshotTurn, ResearchBoardSummary, ResearchBoardDetail, ResearchEventsResponse, ResearchSpawnExtras, ResearchThread, ResearchLead, AuthResponse, PreloginResponse, MyChatRow } from "./types"; const client = axios.create({ baseURL: "/api", }); let activeSessionId: string | null = null; let authToken: string | null = null; export const AUTH_TOKEN_STORAGE_KEY = "riksdagen-auth-token"; /** * Persist the caller’s session id so every request can be associated with the same browser. */ export function setSessionId(sessionId: string): void { activeSessionId = sessionId; client.defaults.headers.common["X-Session-Id"] = sessionId; } /** * Attach (or drop) the account bearer token on every subsequent request. * The token is opaque — it never contains or reveals any encryption key. */ export function setAuthToken(token: string | null): void { authToken = token; if (token) client.defaults.headers.common["Authorization"] = `Bearer ${token}`; else delete client.defaults.headers.common["Authorization"]; } // Eagerly restore the session id + auth token from localStorage at module // load, so requests fired before App's mount effect runs (e.g. the /research // list query) already carry X-Session-Id/Authorization. try { if (typeof window !== "undefined") { const stored = window.localStorage.getItem("riksdagen-session-id"); if (stored) setSessionId(stored); const token = window.localStorage.getItem(AUTH_TOKEN_STORAGE_KEY); if (token) setAuthToken(token); } } catch { /* localStorage unavailable — App's effect will set it */ } /** * Provide fetch-compatible headers carrying the current session id + auth token. */ export function getSessionHeaders(): Record { const headers: Record = {}; if (activeSessionId) headers["X-Session-Id"] = activeSessionId; if (authToken) headers["Authorization"] = `Bearer ${authToken}`; return headers; } /* ── Account auth (zero-knowledge: only derived keys ever sent) ── */ export async function preLogin(username: string): Promise { const { data } = await client.get("/auth/prelogin", { params: { username } }); return data; } export async function signupAccount(payload: { username: string; auth_key: string; kdf_salt: string; kdf_iterations: number; wrapped_dek: string; }): Promise { const { data } = await client.post("/auth/signup", payload); return data; } export async function loginAccount(payload: { username: string; auth_key: string }): Promise { const { data } = await client.post("/auth/login", payload); return data; } export async function logoutAccount(): Promise { await client.post("/auth/logout").catch(() => {}); } export async function fetchMyChats(): Promise { const { data } = await client.get("/me/chats"); return data; } export async function deleteMyChat(id: string): Promise { await client.delete(`/me/chats/${encodeURIComponent(id)}`); } export async function fetchMeta(): Promise { const { data } = await client.get("/meta"); return data; } export async function fetchGuide(): Promise { const response = await fetch("/api/guide", { headers: getSessionHeaders() }); if (!response.ok) throw new Error(`${response.status}`); return response.text(); } export async function searchTalks(payload: SearchRequest): Promise { const { data } = await client.post("/search", payload); return data; } export async function sendFeedback(message: string): Promise { await client.post("/feedback", { message }); } export async function chatWithRiksdagen(payload: ChatRequest): Promise { const response = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json", ...getSessionHeaders(), }, body: JSON.stringify(payload), }); if (!response.ok) { throw new Error("Chat request failed"); } return response.json(); } export async function sendChat( messages: ChatMessage[], focusIds: string[], ): Promise { const response = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json", ...getSessionHeaders(), }, body: JSON.stringify({ messages, focus_ids: focusIds, session_id: activeSessionId, }), }); if (!response.ok) { throw new Error("Chat request failed"); } const data = await response.json(); return { answer: data.answer, sources: data.sources ?? [], tables: data.tables ?? [], focus_ids: data.focus_ids ?? [], }; } /** * Fetches a single talk by its ID, including person information. * * @param id - The talk ID (e.g., "H40911") * @returns Promise containing the talk data with person info */ export async function fetchTalk(id: string): Promise { const response = await fetch(`/api/talk/${encodeURIComponent(id)}`, { headers: getSessionHeaders(), }); if (!response.ok) { throw new Error(`Failed to fetch talk: ${response.status}`); } return response.json(); } /** * Fetches a single motion by its doc_id, including authors and yrkanden. * * @param id - The motion doc_id (e.g., "HD02846"), with or without "documents/" prefix */ export async function fetchMotion(id: string): Promise { const bare = id.startsWith("documents/") ? id.slice("documents/".length) : id; const response = await fetch(`/api/motion/${encodeURIComponent(bare)}`, { headers: getSessionHeaders(), }); if (!response.ok) { throw new Error(`Failed to fetch motion: ${response.statusText}`); } return response.json(); } /** * Load a chat session by UUID. Returns null if not found or expired. */ export async function getSession(uuid: string): Promise { const response = await fetch(`/api/sessions/${encodeURIComponent(uuid)}`, { headers: getSessionHeaders(), }); if (response.status === 404) return null; if (!response.ok) throw new Error(`Failed to load session: ${response.statusText}`); return response.json(); } /** * Create or update a chat session. Fire-and-forget safe (errors are logged only). * Logged-in callers pass enc_payload/enc_title (client-encrypted) instead of * the plaintext fields. */ export async function upsertSession(uuid: string, data: SessionUpsertData): Promise { await fetch(`/api/sessions/${encodeURIComponent(uuid)}`, { method: "PUT", headers: { "Content-Type": "application/json", ...getSessionHeaders() }, body: JSON.stringify(data), }); } /** * Create a frozen shareable snapshot of a chat. Returns the new snapshot UUID. */ export async function createSnapshot(data: { session_type: "general" | "mp"; person_id?: string | null; initial_speech_id?: string | null; llm_messages?: unknown[]; turns: SnapshotTurn[]; focus_ids?: string[]; }): Promise { const response = await fetch("/api/snapshots", { method: "POST", headers: { "Content-Type": "application/json", ...getSessionHeaders() }, body: JSON.stringify(data), }); if (!response.ok) throw new Error(`Failed to create snapshot: ${response.statusText}`); const json = await response.json(); return json.id as string; } /** * Fork a snapshot into a new editable session. Returns the new session UUID. */ export async function forkSnapshot(snapshotId: string): Promise { const response = await fetch(`/api/snapshots/${encodeURIComponent(snapshotId)}/fork`, { method: "POST", headers: getSessionHeaders(), }); if (!response.ok) throw new Error(`Failed to fork snapshot: ${response.statusText}`); const json = await response.json(); return json.session_id as string; } /** * Load a snapshot by UUID. Returns null if not found. */ export async function getSnapshot(uuid: string): Promise { const response = await fetch(`/api/snapshots/${encodeURIComponent(uuid)}`, { headers: getSessionHeaders(), }); if (response.status === 404) return null; if (!response.ok) throw new Error(`Failed to load snapshot: ${response.statusText}`); return response.json(); } /** * Fetches basic info for a Riksdag member by person_id. */ export async function fetchPerson(person_id: string): Promise { const { data } = await client.get(`/person/${encodeURIComponent(person_id)}`); return data; } /* ── Account settings (client-encrypted AI config) ──────────────── */ export async function getMySettings(): Promise<{ enc_settings: string | null; updated_at: string | null }> { const { data } = await client.get("/me/settings"); return data; } export async function putMySettings(encSettings: string): Promise { await client.put("/me/settings", { enc_settings: encSettings }); } export async function deleteMySettings(): Promise { await client.delete("/me/settings"); } /* ── Deep research ─────────────────────────────────────────────── */ export async function createResearch( payload: { topic: string; title?: string; wrapped_board_key?: string } & ResearchSpawnExtras, ): Promise<{ board_id: string; job_id: string }> { const { data } = await client.post("/research", payload); return data; } export async function listResearch(): Promise { const { data } = await client.get("/research"); return data; } export async function getResearchBoard(boardId: string): Promise { const { data } = await client.get(`/research/${encodeURIComponent(boardId)}`); return data; } export async function getResearchEvents( boardId: string, jobId: string, offset: number, ): Promise { const { data } = await client.get( `/research/${encodeURIComponent(boardId)}/events`, { params: { job_id: jobId, offset } }, ); return data; } export async function seedResearchThread( boardId: string, payload: { text: string } & ResearchSpawnExtras, ): Promise<{ thread: ResearchThread; job_id: string | null }> { const { data } = await client.post(`/research/${encodeURIComponent(boardId)}/threads`, payload); return data; } export async function deepenResearch( boardId: string, payload: { thread_id?: string; lead?: ResearchLead; sweep?: boolean } & ResearchSpawnExtras, ): Promise<{ job_id: string }> { const { data } = await client.post(`/research/${encodeURIComponent(boardId)}/deepen`, payload); return data; } export async function activateResearchThreads( boardId: string, payload: { selections: { thread_id: string; guidance?: string }[]; dig?: boolean } & ResearchSpawnExtras, ): Promise<{ activated: string[]; job_id: string | null }> { const { data } = await client.post( `/research/${encodeURIComponent(boardId)}/threads/activate`, { dig: true, ...payload }, ); return data; } export async function archiveResearchThread( boardId: string, threadId: string, ): Promise<{ archived: boolean }> { const { data } = await client.post( `/research/${encodeURIComponent(boardId)}/threads/${encodeURIComponent(threadId)}/archive`, ); return data; } export async function generateResearchReport( boardId: string, payload: ResearchSpawnExtras = {}, ): Promise<{ job_id: string }> { const { data } = await client.post(`/research/${encodeURIComponent(boardId)}/report`, payload); return data; } export async function cancelResearch(boardId: string): Promise<{ cancelled: boolean }> { const { data } = await client.post(`/research/${encodeURIComponent(boardId)}/cancel`); return data; } export async function deleteResearch(boardId: string): Promise { await client.delete(`/research/${encodeURIComponent(boardId)}`); }