"use client"; import { createNotesApiClient } from "@/lib/api-helpers"; import { withMutationRetry } from "@/lib/mutation-retry"; import type { AgentTimelineItem, ArtifactSummary, LinkedNote, NoteArtifactDoc, NoteArtifactListResponse, NoteAgentActionDoc, NoteAgentActionListResponse, NoteDetail, NoteDoc, NoteListResponse, NoteRelationshipDoc, NoteRelationshipListResponse, NoteSummary, NoteTask, NoteTaskDoc, NoteTaskListResponse, WorkspaceDoc, WorkspaceListResponse, WorkspaceSummary, } from "@/lib/types"; function buildWorkspaceMap(workspaces: WorkspaceDoc[]) { return new Map(workspaces.map((workspace) => [workspace.id, workspace])); } function buildNoteMap(notes: NoteDoc[]) { return new Map(notes.map((note) => [note.id, note])); } function toNoteSummary(note: NoteDoc): NoteSummary { return { id: note.id, workspaceId: note.workspaceId, title: note.title, excerpt: note.body.slice(0, 160), status: note.status, tags: note.tags, updatedAt: note.updatedAt, updatedBy: note.updatedBy, }; } function toWorkspaceSummary(workspace: WorkspaceDoc, notes: NoteDoc[]): WorkspaceSummary { const owner = workspace.members.find((member) => member.role === "owner")?.userId ?? workspace.updatedBy; const noteCount = notes.filter((note) => note.workspaceId === workspace.id).length; return { id: workspace.id, name: workspace.name, description: workspace.description ?? "", owner, noteCount, visibility: workspace.members.length > 1 ? "shared" : "private", updatedAt: workspace.updatedAt, tags: [], }; } function toNoteTask(task: NoteTaskDoc): NoteTask { return { id: task.id, title: task.title, status: task.status === "open" ? "todo" : task.status === "completed" ? "done" : "in_progress", source: task.source === "extracted" ? "agent" : "manual", }; } function toArtifactSummary(artifact: NoteArtifactDoc): ArtifactSummary { return { id: artifact.id, name: artifact.title, type: artifact.artifactType, status: artifact.description ? "ready" : "processing", blobPath: artifact.blobPath, contentType: artifact.contentType, sizeBytes: artifact.sizeBytes, }; } function toTimelineItem(action: NoteAgentActionDoc): AgentTimelineItem { return { id: action.id, actor: action.actorId, action: `${action.actorType} ${action.actionType.replaceAll("_", " ")}`, timestamp: action.updatedAt, status: action.state, summary: action.afterSummary ?? action.reason ?? action.actionType, }; } function toReviewState( status: AgentTimelineItem["status"] | undefined ): NoteDetail["metadata"]["reviewState"] { if (status === "proposed" || status === "approved" || status === "rejected") { return status; } return "none"; } function toLinkedNotes( noteId: string, relationships: NoteRelationshipDoc[], noteMap: Map, ): LinkedNote[] { return relationships.flatMap((relationship) => { const relatedNoteId = relationship.fromNoteId === noteId ? relationship.toNoteId : relationship.toNoteId === noteId ? relationship.fromNoteId : null; if (!relatedNoteId) { return []; } const relatedNote = noteMap.get(relatedNoteId); if (!relatedNote) { return []; } return [ { id: relatedNote.id, title: relatedNote.title, relationship: relationship.relationshipType, }, ]; }); } type WorkspaceSummaryDoc = WorkspaceDoc & { noteCount: number }; type WorkspaceSummaryListResponse = { items: WorkspaceSummaryDoc[]; total: number }; export async function listWorkspaceSummaries(): Promise { const api = createNotesApiClient(); const response = await api.fetch("/workspaces/summaries"); return response.items.map((ws) => { const owner = ws.members.find((m) => m.role === "owner")?.userId ?? ws.updatedBy; return { id: ws.id, name: ws.name, description: ws.description ?? "", owner, noteCount: ws.noteCount, visibility: ws.members.length > 1 ? "shared" : "private", updatedAt: ws.updatedAt, tags: [], }; }); } export async function listNoteSummaries(): Promise { const api = createNotesApiClient(); const response = await api.fetch("/notes"); return response.items.map(toNoteSummary); } export async function searchNoteSummaries(query: string): Promise { const api = createNotesApiClient(); const search = query.trim(); const path = search ? `/notes?search=${encodeURIComponent(search)}` : "/notes"; const response = await api.fetch(path); return response.items.map(toNoteSummary); } export interface SearchRankedHit { noteId: string; workspaceId: string; title: string; score: number; matchKind: string; snippet: string; } export async function searchNotesRanked( q: string, mode: "lexical" | "hybrid", options?: { limit?: number; offset?: number; workspaceId?: string }, ): Promise<{ items: SearchRankedHit[]; mode: string; total: number }> { const api = createNotesApiClient(); return api.fetch("/notes/search", { method: "POST", body: JSON.stringify({ q: q.trim(), mode, workspaceId: options?.workspaceId, limit: options?.limit ?? 50, offset: options?.offset ?? 0, }), }); } export async function createNoteShare(noteId: string, workspaceId: string): Promise<{ shareToken: string; path: string }> { const api = createNotesApiClient(); return api.fetch(`/notes/${encodeURIComponent(noteId)}/share`, { method: "POST", body: JSON.stringify({ workspaceId }), }); } export interface NoteVersionRow { id: string; noteId: string; title: string; body: string; savedAt: string; source: string; } export async function listNoteVersions( noteId: string, workspaceId: string, ): Promise<{ items: NoteVersionRow[]; total: number }> { const api = createNotesApiClient(); const qs = new URLSearchParams({ workspaceId, limit: "30", offset: "0" }); return api.fetch(`/notes/${encodeURIComponent(noteId)}/versions?${qs.toString()}`); } export async function seedOnboardingWorkspace(): Promise<{ workspaceId: string; noteIds: string[] }> { const api = createNotesApiClient(); return api.fetch("/workspaces/onboarding-seed", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}), }); } export async function chatOverWorkspace(workspaceId: string, message: string): Promise<{ answer: string; citations: Array<{ noteId: string; title: string; snippet: string; workspaceId: string }>; }> { const api = createNotesApiClient(); return api.fetch("/notes/chat", { method: "POST", body: JSON.stringify({ workspaceId, message }), }); } export async function listNotesForWorkspace(workspaceId: string): Promise { const api = createNotesApiClient(); const response = await api.fetch(`/notes?workspaceId=${encodeURIComponent(workspaceId)}`); return response.items.map(toNoteSummary); } export async function updateNoteDetail( noteId: string, workspaceId: string, updates: { title?: string; body?: string; }, ): Promise { const api = createNotesApiClient(); const path = `/notes/${encodeURIComponent(noteId)}?workspaceId=${encodeURIComponent(workspaceId)}`; await withMutationRetry({ run: () => api.fetch(path, { method: "PATCH", body: JSON.stringify(updates), }), queue: { id: noteId, action: "patch", path, payload: updates }, }); } export async function createNoteArtifact(input: { id: string; workspaceId: string; noteId: string; artifactType: "file" | "summary" | "extraction" | "citation" | "export"; title: string; description?: string; blobPath?: string; contentType?: string; sizeBytes?: number; }): Promise { const api = createNotesApiClient(); await withMutationRetry({ run: () => api.fetch("/note-artifacts", { method: "POST", body: JSON.stringify(input), }), queue: { id: input.id, action: "post", path: "/note-artifacts", payload: input }, }); } export async function createNoteTask(input: { id: string; workspaceId: string; noteId: string; title: string; description?: string; dueAt?: string; source?: "manual" | "extracted"; }): Promise { const api = createNotesApiClient(); await withMutationRetry({ run: () => api.fetch("/note-tasks", { method: "POST", body: JSON.stringify(input), }), queue: { id: input.id, action: "post", path: "/note-tasks", payload: input }, }); } export async function getNoteDetail(noteId: string, knownWorkspaceId?: string): Promise { const api = createNotesApiClient(); let note: NoteDoc | undefined; let workspaceId: string; if (knownWorkspaceId) { try { note = await api.fetch( `/notes/${encodeURIComponent(noteId)}?workspaceId=${encodeURIComponent(knownWorkspaceId)}` ); workspaceId = knownWorkspaceId; } catch { return null; } } else { try { const noteResponse = await api.fetch(`/notes?search=${encodeURIComponent(noteId)}&limit=1`); note = noteResponse.items.find((item) => item.id === noteId); if (!note) return null; workspaceId = note.workspaceId; } catch { return null; } } const wsId = encodeURIComponent(workspaceId); const nId = encodeURIComponent(noteId); const [workspaceResponse, notesForLinked, taskResponse, artifactResponse, actionResponse] = await Promise.all([ api.fetch("/workspaces"), api.fetch(`/notes?workspaceId=${wsId}`), api.fetch(`/note-tasks?workspaceId=${wsId}¬eId=${nId}`), api.fetch(`/note-artifacts?workspaceId=${wsId}¬eId=${nId}`), api.fetch(`/note-agent-actions?workspaceId=${wsId}¬eId=${nId}`), ]); const workspaceMap = buildWorkspaceMap(workspaceResponse.items); const noteMap = buildNoteMap(notesForLinked.items); const workspace = workspaceMap.get(workspaceId); let relationshipResponse: NoteRelationshipListResponse = { items: [] }; try { const fetched = await api.fetch( `/note-relationships?workspaceId=${wsId}¬eId=${nId}` ); relationshipResponse = fetched && Array.isArray(fetched.items) ? fetched : { items: [] }; } catch { relationshipResponse = { items: [] }; } const tasks = taskResponse.items.map(toNoteTask); const artifacts = artifactResponse.items.map(toArtifactSummary); const timeline = actionResponse.items .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)) .map(toTimelineItem); const linkedNotes = toLinkedNotes(note.id, relationshipResponse.items, noteMap); return { ...toNoteSummary(note), body: note.body, metadata: { owner: workspace?.members.find((member) => member.role === "owner")?.userId ?? note.updatedBy, source: note.sourceType ?? "manual", reviewState: toReviewState(timeline[0]?.status), taskCount: tasks.length, artifactCount: artifacts.length, }, linkedNotes, tasks, artifacts, timeline, }; } export async function createNote(input: { id: string; workspaceId: string; title: string; body: string; tags?: string[]; sourceType?: string; }): Promise { const api = createNotesApiClient(); return withMutationRetry({ run: () => api.fetch("/notes", { method: "POST", body: JSON.stringify(input), }), queue: { id: input.id, action: "post", path: "/notes", payload: input }, }); } export async function archiveNote(noteId: string, workspaceId: string): Promise { const api = createNotesApiClient(); const path = `/notes/${encodeURIComponent(noteId)}/archive`; await withMutationRetry({ run: () => api.fetch(path, { method: "POST", body: JSON.stringify({ workspaceId }), }), queue: { id: `${noteId}:archive`, action: "post", path, payload: { workspaceId } }, }); } export async function restoreNote(noteId: string, workspaceId: string): Promise { const api = createNotesApiClient(); const path = `/notes/${encodeURIComponent(noteId)}/restore`; await withMutationRetry({ run: () => api.fetch(path, { method: "POST", body: JSON.stringify({ workspaceId }), }), queue: { id: `${noteId}:restore`, action: "post", path, payload: { workspaceId } }, }); } export async function summarizeNote(noteId: string, workspaceId: string): Promise { const api = createNotesApiClient(); await api.fetch(`/notes/${encodeURIComponent(noteId)}/summarize`, { method: "POST", body: JSON.stringify({ workspaceId }), }); } export async function exportNotes(format: "json" | "markdown", workspaceId?: string): Promise { const api = createNotesApiClient(); const params = new URLSearchParams({ format }); if (workspaceId) params.set("workspaceId", workspaceId); const res = await api.fetch(`/notes/export?${params.toString()}`); return typeof res === "string" ? res : JSON.stringify(res, null, 2); } export async function createWorkspace(input: { id: string; name: string; description?: string; }): Promise { const api = createNotesApiClient(); return withMutationRetry({ run: () => api.fetch("/workspaces", { method: "POST", body: JSON.stringify(input), }), queue: { id: input.id, action: "post", path: "/workspaces", payload: input }, }); } export async function updateWorkspace( workspaceId: string, updates: { name?: string; description?: string }, ): Promise { const api = createNotesApiClient(); const path = `/workspaces/${encodeURIComponent(workspaceId)}`; await withMutationRetry({ run: () => api.fetch(path, { method: "PATCH", body: JSON.stringify(updates), }), queue: { id: workspaceId, action: "patch", path, payload: updates }, }); } export async function deleteWorkspace(workspaceId: string): Promise { const api = createNotesApiClient(); const path = `/workspaces/${encodeURIComponent(workspaceId)}`; await withMutationRetry({ run: () => api.fetch(path, { method: "DELETE", }), queue: { id: workspaceId, action: "delete", path, payload: {} }, }); } export async function createNoteRelationship(input: { id: string; workspaceId: string; fromNoteId: string; toNoteId: string; relationshipType: string; }): Promise { const api = createNotesApiClient(); await withMutationRetry({ run: () => api.fetch("/note-relationships", { method: "POST", body: JSON.stringify(input), }), queue: { id: input.id, action: "post", path: "/note-relationships", payload: input }, }); }