/** * Client for maskinrommet intentions API. * Uses the Vite dev proxy (/api → api.sidelinja.org) in development. * In production, set VITE_API_URL to the maskinrommet URL. */ const BASE_URL = import.meta.env.VITE_API_URL ?? '/api'; export interface CreateNodeRequest { node_kind?: string; title?: string; content?: string; visibility?: string; metadata?: Record; /** Kontekst-node (kommunikasjonsnode). Gir automatisk belongs_to-edge. */ context_id?: string; } export interface CreateNodeResponse { node_id: string; /** Edge-ID for automatisk belongs_to-edge (kun ved context_id). */ belongs_to_edge_id?: string; } export interface CreateEdgeRequest { source_id: string; target_id: string; edge_type: string; metadata?: Record; system?: boolean; } export interface CreateEdgeResponse { edge_id: string; } async function post(accessToken: string, path: string, data: unknown): Promise { const res = await fetch(`${BASE_URL}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}` }, body: JSON.stringify(data) }); if (!res.ok) { const body = await res.text(); throw new Error(`${path} failed (${res.status}): ${body}`); } return res.json(); } export function createNode( accessToken: string, data: CreateNodeRequest ): Promise { return post(accessToken, '/intentions/create_node', data); } export function createEdge( accessToken: string, data: CreateEdgeRequest ): Promise { return post(accessToken, '/intentions/create_edge', data); } // ============================================================================= // Edge-oppdatering // ============================================================================= export interface UpdateEdgeRequest { edge_id: string; edge_type?: string; metadata?: Record; } export interface UpdateEdgeResponse { edge_id: string; } export function updateEdge( accessToken: string, data: UpdateEdgeRequest ): Promise { return post(accessToken, '/intentions/update_edge', data); } // ============================================================================= // Board / Kanban // ============================================================================= export interface BoardCard { node_id: string; title: string | null; content: string | null; node_kind: string; metadata: Record; created_at: string; created_by: string | null; status: string | null; position: number; belongs_to_edge_id: string; status_edge_id: string | null; } export interface BoardResponse { board_id: string; board_title: string | null; columns: string[]; cards: BoardCard[]; } export async function fetchBoard(accessToken: string, boardId: string): Promise { const res = await fetch(`${BASE_URL}/query/board?board_id=${encodeURIComponent(boardId)}`, { headers: { Authorization: `Bearer ${accessToken}` } }); if (!res.ok) { const body = await res.text(); throw new Error(`board failed (${res.status}): ${body}`); } return res.json(); } // ============================================================================= // Kommunikasjon // ============================================================================= export interface CreateCommunicationRequest { title?: string; participants?: string[]; visibility?: string; metadata?: Record; } export interface CreateCommunicationResponse { node_id: string; edge_ids: string[]; } export function createCommunication( accessToken: string, data: CreateCommunicationRequest ): Promise { return post(accessToken, '/intentions/create_communication', data); } export interface UploadMediaRequest { file: File; source_id?: string; visibility?: string; title?: string; } export interface UploadMediaResponse { media_node_id: string; cas_hash: string; size_bytes: number; already_existed: boolean; has_media_edge_id?: string; } export async function uploadMedia( accessToken: string, data: UploadMediaRequest ): Promise { const form = new FormData(); form.append('file', data.file); if (data.source_id) form.append('source_id', data.source_id); if (data.visibility) form.append('visibility', data.visibility); if (data.title) form.append('title', data.title); const res = await fetch(`${BASE_URL}/intentions/upload_media`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}` }, body: form }); if (!res.ok) { const body = await res.text(); throw new Error(`upload_media failed (${res.status}): ${body}`); } return res.json(); } /** Build the CAS URL for a given hash. */ export function casUrl(hash: string): string { return `${BASE_URL}/cas/${hash}`; } // ============================================================================= // Transkripsjons-segmenter // ============================================================================= export interface Segment { id: number; seq: number; start_ms: number; end_ms: number; content: string; edited: boolean; } export interface SegmentsResponse { segments: Segment[]; transcribed_at: string | null; } /** Hent transkripsjons-segmenter for en media-node. */ export async function fetchSegments( accessToken: string, nodeId: string ): Promise { const res = await fetch(`${BASE_URL}/query/segments?node_id=${encodeURIComponent(nodeId)}`, { headers: { Authorization: `Bearer ${accessToken}` } }); if (!res.ok) { const body = await res.text(); throw new Error(`segments failed (${res.status}): ${body}`); } return res.json(); } /** Last ned SRT-fil for en media-node. Trigger filnedlasting i nettleseren. */ export async function downloadSrt(accessToken: string, nodeId: string): Promise { const res = await fetch( `${BASE_URL}/query/segments/srt?node_id=${encodeURIComponent(nodeId)}`, { headers: { Authorization: `Bearer ${accessToken}` } } ); if (!res.ok) { const body = await res.text(); throw new Error(`SRT-eksport feilet (${res.status}): ${body}`); } const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'transcription.srt'; a.click(); URL.revokeObjectURL(url); } /** Oppdater teksten i et transkripsjons-segment. */ export function updateSegment( accessToken: string, segmentId: number, content: string ): Promise<{ segment_id: number; edited: boolean }> { return post(accessToken, '/intentions/update_segment', { segment_id: segmentId, content }); } // ============================================================================= // Re-transkripsjon // ============================================================================= export interface TranscriptionVersion { transcribed_at: string; segment_count: number; edited_count: number; } export interface TranscriptionVersionsResponse { versions: TranscriptionVersion[]; } /** Hent alle transkripsjonsversjoner for en node. */ export async function fetchTranscriptionVersions( accessToken: string, nodeId: string ): Promise { const res = await fetch( `${BASE_URL}/query/transcription_versions?node_id=${encodeURIComponent(nodeId)}`, { headers: { Authorization: `Bearer ${accessToken}` } } ); if (!res.ok) { const body = await res.text(); throw new Error(`transcription_versions failed (${res.status}): ${body}`); } return res.json(); } /** Hent segmenter for en spesifikk transkripsjonsversjon. */ export async function fetchSegmentsVersion( accessToken: string, nodeId: string, transcribedAt: string ): Promise { const params = new URLSearchParams({ node_id: nodeId, transcribed_at: transcribedAt }); const res = await fetch(`${BASE_URL}/query/segments_version?${params}`, { headers: { Authorization: `Bearer ${accessToken}` } }); if (!res.ok) { const body = await res.text(); throw new Error(`segments_version failed (${res.status}): ${body}`); } return res.json(); } /** Trigger re-transkripsjon for en media-node. */ export function retranscribe( accessToken: string, nodeId: string ): Promise<{ job_id: string }> { return post(accessToken, '/intentions/retranscribe', { node_id: nodeId }); } export interface SegmentChoice { seq: number; choice: 'new' | 'old'; } /** Anvend brukerens per-segment-valg etter re-transkripsjon. */ export function resolveRetranscription( accessToken: string, nodeId: string, newVersion: string, oldVersion: string, choices: SegmentChoice[] ): Promise<{ resolved: boolean; kept_old: number; kept_new: number }> { return post(accessToken, '/intentions/resolve_retranscription', { node_id: nodeId, new_version: newVersion, old_version: oldVersion, choices }); }