synops/frontend/src/lib/api.ts
vegard dee9d5bf3a Fullfør oppgave 6.4: Bilder i TipTap via drag-and-drop/paste
Brukeren kan nå dra eller lime inn bilder i TipTap-editoren.
Bildet lastes opp til CAS via upload_media-endepunktet, og settes
inn som <img> med CAS-URL i metadata.document (HTML).

Endringer:
- Ny uploadMedia() og casUrl() i api.ts for multipart upload
- @tiptap/extension-image med CasImage-utvidelse (data-node-id attr)
- handleDrop/handlePaste i editor intercepter bildefiler
- Upload-status vises i editoren mens bilder lastes opp
- accessToken sendes ned til NodeEditor fra +page.svelte

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 17:07:17 +01:00

130 lines
3.1 KiB
TypeScript

/**
* 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<string, unknown>;
/** 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<string, unknown>;
system?: boolean;
}
export interface CreateEdgeResponse {
edge_id: string;
}
async function post<T>(accessToken: string, path: string, data: unknown): Promise<T> {
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<CreateNodeResponse> {
return post(accessToken, '/intentions/create_node', data);
}
export function createEdge(
accessToken: string,
data: CreateEdgeRequest
): Promise<CreateEdgeResponse> {
return post(accessToken, '/intentions/create_edge', data);
}
export interface CreateCommunicationRequest {
title?: string;
participants?: string[];
visibility?: string;
metadata?: Record<string, unknown>;
}
export interface CreateCommunicationResponse {
node_id: string;
edge_ids: string[];
}
export function createCommunication(
accessToken: string,
data: CreateCommunicationRequest
): Promise<CreateCommunicationResponse> {
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<UploadMediaResponse> {
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}`;
}